16
16
https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotification
17
17
"""
18
18
19
+ from __future__ import annotations
20
+ import contextlib
19
21
import ctypes
22
+ from contextlib import contextmanager
20
23
from ctypes .wintypes import (
21
24
HANDLE ,
22
25
HWND ,
25
28
from typing import (
26
29
Dict ,
27
30
Set ,
31
+ Optional ,
32
+ )
33
+ from winAPI .wtsApi32 import (
34
+ WTSINFOEXW ,
35
+ WTSQuerySessionInformation ,
36
+ WTS_CURRENT_SERVER_HANDLE ,
37
+ WTS_CURRENT_SESSION ,
38
+ WTS_INFO_CLASS ,
39
+ WTSFreeMemory ,
40
+ WTS_LockState ,
41
+ WTSINFOEX_LEVEL1_W ,
28
42
)
29
-
30
43
from logHandler import log
31
44
32
-
33
45
_currentSessionStates : Set ["WindowsTrackedSession" ] = set ()
34
46
"""
35
47
Current state of the Windows session associated with this instance of NVDA.
36
48
Maintained via receiving session notifications via the NVDA MessageWindow.
49
+ Initial state will be set by querying the current status.
37
50
"""
38
51
52
+ _sessionQueryLockStateHasBeenUnknown = False
53
+ """
54
+ Track if any 'Unknown' Value when querying the Session Lock status has been encountered.
55
+ """
56
+
57
+ _isSessionTrackingRegistered = False
58
+ """
59
+ Session tracking is required for NVDA to be notified of lock state changes for security purposes.
60
+ """
39
61
40
62
RPC_S_INVALID_BINDING = 0x6A6
41
63
"""
@@ -61,7 +83,8 @@ class WindowsTrackedSession(enum.IntEnum):
61
83
Windows Tracked Session notifications.
62
84
Members are states which form logical pairs,
63
85
except SESSION_REMOTE_CONTROL which requires more complex handling.
64
-
86
+ Values from: https://learn.microsoft.com/en-us/windows/win32/termserv/wm-wtssession-change
87
+ Context:
65
88
https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotification
66
89
"""
67
90
CONSOLE_CONNECT = 1
@@ -95,13 +118,91 @@ class WindowsTrackedSession(enum.IntEnum):
95
118
"""
96
119
97
120
121
+ def _hasLockStateBeenTracked () -> bool :
122
+ """
123
+ Checks if NVDA is aware of a session lock state change since NVDA started.
124
+ """
125
+ return bool (_currentSessionStates .intersection ({
126
+ WindowsTrackedSession .SESSION_LOCK ,
127
+ WindowsTrackedSession .SESSION_UNLOCK
128
+ }))
129
+
130
+
131
+ def _recordLockStateTrackingFailure (error : Optional [Exception ] = None ):
132
+ log .error (
133
+ "Unknown lock state, unexpected, potential security issue, please report." ,
134
+ exc_info = error
135
+ ) # Report error repeatedly, attention is required.
136
+ ##
137
+ # For security it would be best to treat unknown as locked.
138
+ # However, this would make NVDA unusable.
139
+ # Instead, the user should be warned, and allowed to mitigate the impact themselves.
140
+ # Reporting is achieved via _sessionQueryLockStateHasBeenUnknown exposed with
141
+ # L{hasLockStateBeenUnknown}.
142
+ global _sessionQueryLockStateHasBeenUnknown
143
+ _sessionQueryLockStateHasBeenUnknown = True
144
+
145
+
98
146
def isWindowsLocked () -> bool :
99
147
"""
100
148
Checks if the Window lockscreen is active.
101
-
102
149
Not to be confused with the Windows sign-in screen, a secure screen.
103
150
"""
104
- return WindowsTrackedSession .SESSION_LOCK in _currentSessionStates
151
+ lockStateTracked = _hasLockStateBeenTracked ()
152
+ if lockStateTracked :
153
+ return WindowsTrackedSession .SESSION_LOCK in _currentSessionStates
154
+ else :
155
+ _recordLockStateTrackingFailure () # Report error repeatedly, attention is required.
156
+ ##
157
+ # For security it would be best to treat unknown as locked.
158
+ # However, this would make NVDA unusable.
159
+ # Instead, the user should be warned via UI, and allowed to mitigate the impact themselves.
160
+ # See usage of L{hasLockStateBeenUnknown}.
161
+ return False # return False, indicating unlocked, to allow NVDA to be used
162
+
163
+
164
+ def _setInitialWindowLockState () -> None :
165
+ """Ensure that session tracking state is initialized.
166
+ If NVDA has started on a lockScreen, it needs to be aware of this.
167
+ """
168
+ lockStateTracked = _hasLockStateBeenTracked ()
169
+ if lockStateTracked :
170
+ raise RuntimeError ("Can't set initial state if it is already tracked." )
171
+ # Fall back to explicit query
172
+ try :
173
+ isLocked = _isWindowsLocked_checkViaSessionQuery ()
174
+ _currentSessionStates .add (
175
+ WindowsTrackedSession .SESSION_LOCK
176
+ if isLocked
177
+ else WindowsTrackedSession .SESSION_UNLOCK
178
+ )
179
+ except RuntimeError as error :
180
+ _recordLockStateTrackingFailure (error )
181
+
182
+
183
+ def _isWindowsLocked_checkViaSessionQuery () -> bool :
184
+ """ Use a session query to check if the session is locked
185
+ @return: True is the session is locked.
186
+ @raise: Runtime error if the lock state can not be determined via a Session Query.
187
+ """
188
+ sessionQueryLockState = _getSessionLockedValue ()
189
+ if sessionQueryLockState == WTS_LockState .WTS_SESSIONSTATE_UNKNOWN :
190
+ raise RuntimeError (
191
+ "Unable to determine lock state via Session Query."
192
+ f" Lock state value: { sessionQueryLockState !r} "
193
+ )
194
+ return sessionQueryLockState == WTS_LockState .WTS_SESSIONSTATE_LOCK
195
+
196
+
197
+ def isLockStateSuccessfullyTracked () -> bool :
198
+ """Check if the lock state is successfully tracked.
199
+ I.E. Registered for session tracking AND initial value set correctly.
200
+ @return: True when successfully tracked.
201
+ """
202
+ return (
203
+ not _sessionQueryLockStateHasBeenUnknown
204
+ or not _isSessionTrackingRegistered
205
+ )
105
206
106
207
107
208
def register (handle : HWND ) -> bool :
@@ -121,6 +222,11 @@ def register(handle: HWND) -> bool:
121
222
122
223
https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotification
123
224
"""
225
+ ##
226
+ # Ensure that an initial state is set,
227
+ # do this first to prevent a race condition with session tracking / message processing.
228
+ _setInitialWindowLockState ()
229
+
124
230
# OpenEvent handle must be closed with CloseHandle.
125
231
eventObjectHandle : HANDLE = ctypes .windll .kernel32 .OpenEventW (
126
232
# Blocks until WTS session tracking can be registered.
@@ -154,7 +260,9 @@ def register(handle: HWND) -> bool:
154
260
else :
155
261
log .error ("Unexpected error registering session tracking." , exc_info = error )
156
262
157
- return registrationSuccess
263
+ global _isSessionTrackingRegistered
264
+ _isSessionTrackingRegistered = registrationSuccess
265
+ return isLockStateSuccessfullyTracked ()
158
266
159
267
160
268
def unregister (handle : HWND ) -> None :
@@ -164,6 +272,9 @@ def unregister(handle: HWND) -> None:
164
272
165
273
https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsunregistersessionnotification
166
274
"""
275
+ if not _isSessionTrackingRegistered :
276
+ log .info ("Not unregistered session tracking, it was not registered." )
277
+ return
167
278
if ctypes .windll .wtsapi32 .WTSUnRegisterSessionNotification (handle ):
168
279
log .debug ("Unregistered session tracking" )
169
280
else :
@@ -187,6 +298,9 @@ def handleSessionChange(newState: WindowsTrackedSession, sessionId: int) -> None
187
298
188
299
log .debug (f"Windows Session state notification received: { newState .name } " )
189
300
301
+ if not _isSessionTrackingRegistered :
302
+ log .debugWarning ("Session tracking not registered, unexpected session change message" )
303
+
190
304
if newState not in _toggleWindowsSessionStatePair :
191
305
log .debug (f"Ignoring { newState } event as tracking is not required." )
192
306
return
@@ -207,3 +321,81 @@ def handleSessionChange(newState: WindowsTrackedSession, sessionId: int) -> None
207
321
log .debug (f"NVDA started in state { oppositeState .name } or dropped a state change event" )
208
322
209
323
log .debug (f"New Windows Session state: { _currentSessionStates } " )
324
+
325
+
326
+ @contextmanager
327
+ def WTSCurrentSessionInfoEx () -> contextlib .AbstractContextManager [ctypes .pointer [WTSINFOEXW ]]:
328
+ """Context manager to get the WTSINFOEXW for the current server/session or raises a RuntimeError.
329
+ Handles freeing the memory when usage is complete.
330
+ """
331
+ info = _getCurrentSessionInfoEx ()
332
+ try :
333
+ yield info
334
+ finally :
335
+ WTSFreeMemory (
336
+ ctypes .cast (info , ctypes .c_void_p ),
337
+ )
338
+
339
+
340
+ def _getCurrentSessionInfoEx () -> ctypes .POINTER (WTSINFOEXW ):
341
+ """ Gets the WTSINFOEXW for the current server/session or raises a RuntimeError
342
+ on failure.
343
+ On RuntimeError memory is first freed.
344
+ In other cases use WTSFreeMemory.
345
+ Ideally use the WTSCurrentSessionInfoEx context manager which will handle freeing the memory.
346
+ """
347
+ ppBuffer = ctypes .wintypes .LPWSTR (None )
348
+ pBytesReturned = ctypes .wintypes .DWORD (0 )
349
+
350
+ res = WTSQuerySessionInformation (
351
+ WTS_CURRENT_SERVER_HANDLE , # WTS_CURRENT_SERVER_HANDLE to indicate the RD Session Host server on
352
+ # which the application is running.
353
+ WTS_CURRENT_SESSION , # To indicate the session in which the calling application is running
354
+ # (or the current session) specify WTS_CURRENT_SESSION
355
+ WTS_INFO_CLASS .WTSSessionInfoEx , # Indicates the type of session information to retrieve
356
+ # Fetch a WTSINFOEXW containing a WTSINFOEX_LEVEL1 structure.
357
+ ctypes .pointer (ppBuffer ), # A pointer to a variable that receives a pointer to the requested information.
358
+ # The format and contents of the data depend on the information class specified in the WTSInfoClass
359
+ # parameter.
360
+ # To free the returned buffer, call the WTSFreeMemory function.
361
+ ctypes .pointer (pBytesReturned ), # A pointer to a variable that receives the size, in bytes, of the data
362
+ # returned in ppBuffer.
363
+ )
364
+ try :
365
+ if not res :
366
+ raise RuntimeError (f"Failure calling WTSQuerySessionInformationW: { res } " )
367
+ elif ctypes .sizeof (WTSINFOEXW ) != pBytesReturned .value :
368
+ raise RuntimeError (
369
+ f"Returned data size failure, got { pBytesReturned .value } , expected { ctypes .sizeof (WTSINFOEXW )} "
370
+ )
371
+ info = ctypes .cast (
372
+ ppBuffer ,
373
+ ctypes .POINTER (WTSINFOEXW )
374
+ )
375
+ if (
376
+ not info .contents
377
+ or info .contents .Level != 1
378
+ ##
379
+ # Level value must be 1, see:
380
+ # https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/ns-wtsapi32-wtsinfoexa
381
+ ):
382
+ raise RuntimeError (
383
+ f"Unexpected Level data, got { info .contents .Level } ."
384
+ )
385
+ return info
386
+ except Exception as e :
387
+ log .exception ("Unexpected WTSQuerySessionInformation value:" , exc_info = e )
388
+ WTSFreeMemory (
389
+ ctypes .cast (ppBuffer , ctypes .c_void_p ),
390
+ )
391
+
392
+
393
+ def _getSessionLockedValue () -> WTS_LockState :
394
+ """Get the WTS_LockState for the current server/session or raises a RuntimeError
395
+ """
396
+ with WTSCurrentSessionInfoEx () as info :
397
+ infoEx : WTSINFOEX_LEVEL1_W = info .contents .Data .WTSInfoExLevel1
398
+ sessionFlags : ctypes .wintypes .LONG = infoEx .SessionFlags
399
+ lockState = WTS_LockState (sessionFlags )
400
+ log .debug (f"Query Lock state result: { lockState !r} " )
401
+ return lockState
0 commit comments