Skip to content

Commit

Permalink
Check Z-Order for making content accessible on the lock screen. (#14416)
Browse files Browse the repository at this point in the history
Link to issue number:
Supercedes #14358
Fixes: #14379
Fixes: #14368

Summary of the issue:
Issue 1: Session tracking notification failures (#14358)
If NVDA freezes, session tracking notifications may be dropped.
If this happens when locking, NVDA will be insecure while on the Windows lock screen.
If this happens when unlocking, NVDA will not behave correctly and be inaccessible while Windows is unlocked.

This is fixed by querying the session directly, and caching this every core cycle.
If a query fails, NVDA should fall back accessible behaviour, rather than secure.

Issue 2: Forgot my PIN workflow is inaccessible (#14368)
NVDA cannot read content on the forgot my PIN workflow screen.
This is a similar situation to the lock screen, except an Edge Spartan window is used for the workflow.
This runs on a temporary user profile.

This is fixed by detecting the z-order of windows, and making an window above the lock screen window accessible.

Issue 2a: Object navigation does not work on the PIN workflow screen (#14416)
This is because TextInfo.obj can be a TreeInterceptor, where it was previously documented as just NVDAObject.
This assumption caused the _isSecureObjectWhileLockScreenActivated function to fail, making object navigation fail.
In those cases, the TreeInterceptor.rootNVDAObject should be checked instead.

Issue 3: NVDA fails to install in some environments (#14379)
Sometimes an NVDA session query returns an unexpected value.
In this case, default to the "session unknown" behaviour.
If a session query fails, NVDA should roll back to accessible behaviour rather than failing to run.

Description of user facing changes
PIN workflow screen should become accessible.
NVDA has better session tracking management (i.e. is aware of the lock state more accuractely).
NVDA should handle session query failures without preventing installation, blocking usage, etc.

Description of development approach
There are 4 security modes of NVDA:

normal, authenticated user
secure mode: secure desktop mode (when serviceDebug param not set), or --secure param provided.
Refer to existing docs on this.
secure desktop mode: enabled secure mode (when serviceDebug param not set).
Also prevents access to some controls, that should not be accessible from the sign-in/UAC dialog.
lock screen mode: prevents access to user data. Used on the lock screen, which runs on a user desktop.
Also include the reset PIN workflow and out of box experience. (Only Win 10+)
Lock state session tracking is handled by NVDA now, by querying the session state every core pump cycle.

When on lock screen mode, we need to check the z-order of windows to confirm if an NVDA object should be accessible.
The window associated with the NVDAObject should be above the lowest lock screen window.
Lock screen windows can be identified by known class names.
This is risky as class names may change, but the lockapp appModule isn't detectable on the forgot PIN workflow.

We can confirm the windows order by starting at the lowest lock screen, then navigating our way up.
If we can confirm that the NVDA Object is not below the lock screen window, we can make the object accessible.
This method is risky, as z-ordering is dynamic.
There are unit tests to cover this, code aims to make NVDAObjects accessible where the order is unknown.
  • Loading branch information
seanbudd authored Dec 16, 2022
1 parent 9823556 commit 56070da
Show file tree
Hide file tree
Showing 20 changed files with 889 additions and 363 deletions.
6 changes: 3 additions & 3 deletions source/NVDAHelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import api
import globalVars
from logHandler import log
from utils.security import isWindowsLocked
from utils.security import _isLockScreenModeActive

versionedLibPath = os.path.join(globalVars.appDir, 'lib')
if os.environ.get('PROCESSOR_ARCHITEW6432') == 'ARM64':
Expand Down Expand Up @@ -455,7 +455,7 @@ def nvdaControllerInternal_installAddonPackageFromPath(addonPath):
if globalVars.appArgs.secure:
log.debugWarning("Unable to install add-on into secure copy of NVDA.")
return
if isWindowsLocked():
if _isLockScreenModeActive():
log.debugWarning("Unable to install add-on while Windows is locked.")
return
import wx
Expand All @@ -470,7 +470,7 @@ def nvdaControllerInternal_openConfigDirectory():
if globalVars.appArgs.secure:
log.debugWarning("Unable to open user config directory for secure copy of NVDA.")
return
if isWindowsLocked():
if _isLockScreenModeActive():
log.debugWarning("Unable to open user config directory while Windows is locked.")
return
import systemUtils
Expand Down
13 changes: 11 additions & 2 deletions source/NVDAObjects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@
TreeInterceptor,
)
import braille
from utils.security import _isObjectBelowLockScreen
import vision
import globalPluginHandler
import brailleInput
import locationHelper
import aria
from winAPI.sessionTracking import isWindowsLocked
from winAPI.sessionTracking import _isLockScreenModeActive


class NVDAObjectTextInfo(textInfos.offsets.OffsetsTextInfo):
Expand Down Expand Up @@ -181,7 +182,7 @@ def _insertLockScreenObject(self, clsList: typing.List["NVDAObject"]) -> None:
Inserts LockScreenObject to the start of the clsList if Windows is locked.
"""
from .lockscreen import LockScreenObject
if isWindowsLocked():
if _isLockScreenModeActive():
# This must be resolved first to prevent object navigation outside of the lockscreen.
clsList.insert(0, LockScreenObject)

Expand Down Expand Up @@ -1433,3 +1434,11 @@ def getSelectedItemsCount(self,maxCount=2):
For performance, this method will only count up to the given maxCount number, and if there is one more above that, then sys.maxint is returned stating that many items are selected.
"""
return 0

#: Type definition for auto prop '_get_isBelowLockScreen'
isBelowLockScreen: bool

def _get_isBelowLockScreen(self) -> bool:
if not _isLockScreenModeActive():
return False
return _isObjectBelowLockScreen(self)
19 changes: 17 additions & 2 deletions source/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,23 @@ def setReviewPosition(
@param isCaret: Whether the review position is changed due to caret following.
@param isMouse: Whether the review position is changed due to mouse following.
"""
if _isSecureObjectWhileLockScreenActivated(reviewPosition.obj):
return False
reviewObj = reviewPosition.obj

if isinstance(reviewObj, treeInterceptorHandler.DocumentTreeInterceptor):
# reviewPosition.obj can be a number of classes, e.g.
# CursorManager, DocumentWithTableNavigation, EditableText.
# We can only handle the NVDAObject case.
reviewObj = reviewObj.rootNVDAObject

if isinstance(reviewObj, NVDAObjects.NVDAObject):
# reviewPosition.obj can be a number of classes, e.g.
# CursorManager, DocumentWithTableNavigation, EditableText.
# We can only handle the NVDAObject case.
if _isSecureObjectWhileLockScreenActivated(reviewObj):
return False
else:
log.debug(f"Unhandled reviewObj type {type(reviewObj)} when checking security of reviewObj")

globalVars.reviewPosition=reviewPosition.copy()
globalVars.reviewPositionObj=reviewPosition.obj
if clearNavigatorObject: globalVars.navigatorObject=None
Expand Down
10 changes: 5 additions & 5 deletions source/appModules/lockapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
from NVDAObjects.lockscreen import LockScreenObject
from NVDAObjects.UIA import UIA
from utils.security import getSafeScripts
from winAPI.sessionTracking import isWindowsLocked
from winAPI.sessionTracking import _isLockScreenModeActive

"""App module for the Windows 10 and 11 lock screen.
The lock screen allows other windows to be opened, so security related functions
are done at a higher level than the lockapp app module.
Refer to usages of `winAPI.sessionTracking.isWindowsLocked`.
Refer to usages of `winAPI.sessionTracking._isLockScreenModeActive`.
"""


Expand Down Expand Up @@ -60,11 +60,11 @@ def chooseNVDAObjectOverlayClasses(
if isinstance(obj,UIA) and obj.role==controlTypes.Role.PANE and obj.UIAElement.cachedClassName=="LockAppContainer":
clsList.insert(0,LockAppContainer)

if not isWindowsLocked():
if not _isLockScreenModeActive():
log.debugWarning(
"LockApp is being initialized but NVDA does not expect Windows to be locked. "
"DynamicNVDAObjectType may have failed to apply LockScreenObject. "
"This means Windows session tracking has failed or NVDA is yet to receive lock event. "
"This means session lock state tracking has failed. "
)
clsList.insert(0, LockScreenObject)

Expand All @@ -83,7 +83,7 @@ def _inputCaptor(self, gesture: inputCore.InputGesture) -> bool:
if not scriptShouldRun:
log.error(
"scriptHandler failed to block script when Windows is locked. "
"This means Windows session tracking has failed. "
"This means session lock state tracking has failed. "
)
return scriptShouldRun

Expand Down
30 changes: 26 additions & 4 deletions source/baseObject.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2007-2020 NV Access Limited, Christopher Toth, Babbage B.V., Julien Cochuyt
# Copyright (C) 2007-2022 NV Access Limited, Christopher Toth, Babbage B.V., Julien Cochuyt
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

"""Contains the base classes that many of NVDA's classes such as NVDAObjects, virtualBuffers, appModules, synthDrivers inherit from. These base classes provide such things as auto properties, and methods and properties for scripting and key binding.
"""

from typing import (
Any,
Callable,
Optional,
Set,
Union,
)
import weakref
import garbageHandler
from logHandler import log
from abc import ABCMeta, abstractproperty

GetterReturnT = Any
GetterMethodT = Callable[["AutoPropertyObject"], GetterReturnT]


class Getter(object):

def __init__(self,fget, abstract=False):
self.fget=fget
if abstract:
self._abstract = self.__isabstractmethod__ = abstract

def __get__(self,instance,owner):
def __get__(
self,
instance: Union[Any, None, "AutoPropertyObject"],
owner,
) -> Union[GetterReturnT, "Getter"]:
if isinstance(self.fget, classmethod):
return self.fget.__get__(instance, owner)()
elif instance is None:
Expand All @@ -31,16 +46,22 @@ def setter(self,func):
def deleter(self,func):
return (abstractproperty if self._abstract else property)(fget=self.fget,fdel=func)


class CachingGetter(Getter):

def __get__(self, instance, owner):
def __get__(
self,
instance: Union[Any, None, "AutoPropertyObject"],
owner,
) -> Union[GetterReturnT, "CachingGetter"]:
if isinstance(self.fget, classmethod):
log.warning("Class properties do not support caching")
return self.fget.__get__(instance, owner)()
elif instance is None:
return self
return instance._getPropertyViaCache(self.fget)


class AutoPropertyType(ABCMeta):

def __init__(self,name,bases,dict):
Expand Down Expand Up @@ -125,6 +146,7 @@ class AutoPropertyObject(garbageHandler.TrackedObject, metaclass=AutoPropertyTyp
#: @type: bool
cachePropertiesByDefault = False

_propertyCache: Set[GetterMethodT]

def __new__(cls, *args, **kwargs):
self = super(AutoPropertyObject, cls).__new__(cls)
Expand All @@ -134,7 +156,7 @@ def __new__(cls, *args, **kwargs):
self.__instances[self]=None
return self

def _getPropertyViaCache(self,getterMethod=None):
def _getPropertyViaCache(self, getterMethod: Optional[GetterMethodT] = None) -> GetterReturnT:
if not getterMethod:
raise ValueError("getterMethod is None")
missing=False
Expand Down
8 changes: 4 additions & 4 deletions source/brailleViewer/brailleViewerGui.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import fonts
import inputCore
import gui.contextHelp
from utils.security import isWindowsLocked, postSessionLockStateChanged
from utils.security import _isLockScreenModeActive, postSessionLockStateChanged

BRAILLE_UNICODE_PATTERNS_START = 0x2800
BRAILLE_SPACE_CHARACTER = chr(BRAILLE_UNICODE_PATTERNS_START)
Expand Down Expand Up @@ -398,7 +398,7 @@ def _createControls(self, sizer: wx.Sizer, parent: wx.Control) -> None:
self._shouldShowOnStartupCheckBox.SetValue(config.conf["brailleViewer"]["showBrailleViewerAtStartup"])
self._shouldShowOnStartupCheckBox.Bind(wx.EVT_CHECKBOX, self._onShouldShowOnStartupChanged)
optionsSizer.Add(self._shouldShowOnStartupCheckBox)
if isWindowsLocked():
if _isLockScreenModeActive():
self._shouldShowOnStartupCheckBox.Disable()

# Translators: The label for a setting in the braille viewer that controls
Expand All @@ -415,11 +415,11 @@ def _createControls(self, sizer: wx.Sizer, parent: wx.Control) -> None:
sizer.Add(optionsSizer, flag=wx.EXPAND | wx.TOP, border=5)

def _onShouldShowOnStartupChanged(self, evt: wx.CommandEvent):
if not isWindowsLocked():
if not _isLockScreenModeActive():
config.conf["brailleViewer"]["showBrailleViewerAtStartup"] = self._shouldShowOnStartupCheckBox.IsChecked()

def _onShouldHoverRouteToCellCheckBoxChanged(self, evt: wx.CommandEvent):
if not isWindowsLocked():
if not _isLockScreenModeActive():
config.conf["brailleViewer"]["shouldHoverRouteToCell"] = self._shouldHoverRouteToCellCheckBox.IsChecked()
self._updateMouseOverBinding(self._shouldHoverRouteToCellCheckBox.IsChecked())

Expand Down
26 changes: 7 additions & 19 deletions source/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,13 +451,13 @@ class _TrackNVDAInitialization:
regardless of lock state.
Security checks may cause the desktop object to not be set if NVDA starts on the lock screen.
As such, during initialization, NVDA should behave as if Windows is unlocked,
i.e. winAPI.sessionTracking.isWindowsLocked should return False.
i.e. winAPI.sessionTracking._isLockScreenModeActive should return False.
TODO: move to NVDAState module
"""

_isNVDAInitialized = False
"""When False, isWindowsLocked is forced to return False.
"""When False, _isLockScreenModeActive is forced to return False.
"""

@staticmethod
Expand Down Expand Up @@ -601,7 +601,6 @@ def onEndSession(evt):
wx.CallAfter(audioDucking.initialize)

from winAPI.messageWindow import WindowMessage
from winAPI import sessionTracking
import winUser
# #3763: In wxPython 3, the class name of frame windows changed from wxWindowClassNR to wxWindowNR.
# NVDA uses the main frame to check for and quit another instance of NVDA.
Expand Down Expand Up @@ -630,27 +629,12 @@ def __init__(self, windowName=None):
self.orientationCoordsCache = (0,0)
self.handlePowerStatusChange()

# Call must be paired with a call to sessionTracking.unregister
if not sessionTracking.register(self.handle):
import utils.security
wx.CallAfter(utils.security.warnSessionLockStateUnknown)

def destroy(self):
"""
NVDA must unregister session tracking before destroying the message window.
"""
# Requires an active message window and a handle to unregister.
sessionTracking.unregister(self.handle)
super().destroy()

def windowProc(self, hwnd, msg, wParam, lParam):
post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam)
if msg == WindowMessage.POWER_BROADCAST and wParam == self.PBT_APMPOWERSTATUSCHANGE:
self.handlePowerStatusChange()
elif msg == winUser.WM_DISPLAYCHANGE:
self.handleScreenOrientationChange(lParam)
elif msg == WindowMessage.WTS_SESSION_CHANGE:
sessionTracking.handleSessionChange(sessionTracking.WindowsTrackedSession(wParam), lParam)

def handleScreenOrientationChange(self, lParam):
# TODO: move to winAPI
Expand Down Expand Up @@ -809,7 +793,8 @@ def run(self):
mouseHandler.pumpAll()
braille.pumpAll()
vision.pumpAll()
except:
sessionTracking.pumpAll()
except Exception:
log.exception("errors in this core pump cycle")
baseObject.AutoPropertyObject.invalidateCaches()
watchdog.asleep()
Expand All @@ -832,6 +817,9 @@ def run(self):
log.debug("initializing updateCheck")
updateCheck.initialize()

from winAPI import sessionTracking
sessionTracking.initialize()

_TrackNVDAInitialization.markInitializationComplete()

log.info("NVDA initialized")
Expand Down
3 changes: 2 additions & 1 deletion source/globalVars.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import typing

if typing.TYPE_CHECKING:
import documentBase # noqa: F401 used for type checking only
import NVDAObjects # noqa: F401 used for type checking only


Expand Down Expand Up @@ -70,7 +71,7 @@ class DefaultAppArgs(argparse.Namespace):
mouseOldY=None
navigatorObject: typing.Optional['NVDAObjects.NVDAObject'] = None
reviewPosition=None
reviewPositionObj=None
reviewPositionObj: typing.Optional["documentBase.TextContainerObject"] = None
lastProgressValue=0
appArgs = DefaultAppArgs()
unknownAppArgs: typing.List[str] = []
Expand Down
13 changes: 10 additions & 3 deletions source/nvda.pyw
Original file line number Diff line number Diff line change
Expand Up @@ -350,13 +350,20 @@ if mutex is None:
sys.exit(1)


if _isSecureDesktop():
def _serviceDebugEnabled() -> bool:
import winreg
try:
k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\NVDA")
if not winreg.QueryValueEx(k, u"serviceDebug")[0]:
globalVars.appArgs.secure = True
if winreg.QueryValueEx(k, "serviceDebug")[0]:
return True
except WindowsError:
# Expected state by default, serviceDebug parameter not set
pass
return False


if _isSecureDesktop():
if not _serviceDebugEnabled():
globalVars.appArgs.secure = True
globalVars.appArgs.changeScreenReaderFlag = False
globalVars.appArgs.minimal = True
Expand Down
4 changes: 2 additions & 2 deletions source/scriptHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ def getGlobalMapScripts(gesture: "inputCore.InputGesture") -> List["inputCore.In

def findScript(gesture: "inputCore.InputGesture") -> Optional[_ScriptFunctionT]:
from utils.security import getSafeScripts
from winAPI.sessionTracking import isWindowsLocked
from winAPI.sessionTracking import _isLockScreenModeActive
foundScript = _findScript(gesture)
if (
foundScript is not None
and isWindowsLocked()
and _isLockScreenModeActive()
and foundScript not in getSafeScripts()
):
return None
Expand Down
6 changes: 3 additions & 3 deletions source/speechViewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from logHandler import log
from speech import SpeechSequence
import gui.contextHelp
from utils.security import isWindowsLocked, postSessionLockStateChanged
from utils.security import _isLockScreenModeActive, postSessionLockStateChanged


# Inherit from wx.Frame because these windows show in the alt+tab menu (where miniFrame does not)
Expand Down Expand Up @@ -102,7 +102,7 @@ def _createControls(self, sizer, parent):
wx.EVT_CHECKBOX,
self.onShouldShowOnStartupChanged
)
if isWindowsLocked():
if _isLockScreenModeActive():
self.shouldShowOnStartupCheckBox.Disable()

def _onDialogActivated(self, evt):
Expand All @@ -119,7 +119,7 @@ def onClose(self, evt):
deactivate()

def onShouldShowOnStartupChanged(self, evt: wx.CommandEvent):
if not isWindowsLocked():
if not _isLockScreenModeActive():
config.conf["speechViewer"]["showSpeechViewerAtStartup"] = self.shouldShowOnStartupCheckBox.IsChecked()

_isDestroyed: bool
Expand Down
Loading

0 comments on commit 56070da

Please sign in to comment.