Skip to content

Commit effe778

Browse files
committed
Provide actionable messages for 2 dpapi errors
Add underlying OSError info into Persistence exceptions Refer to the PersistenceDecryptionError wiki page Update msal_extensions/windows.py Co-authored-by: Jiashuo Li <[email protected]> Ignore consider-using-f-string, once and for all
1 parent b674b6a commit effe778

File tree

5 files changed

+66
-13
lines changed

5 files changed

+66
-13
lines changed

Diff for: .pylintrc

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
good-names=
33
logger
44
disable=
5+
consider-using-f-string, # For Python < 3.6
56
super-with-arguments, # For Python 2.x
67
raise-missing-from, # For Python 2.x
78
trailing-newlines,

Diff for: msal_extensions/persistence.py

+36-8
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,31 @@ def _auto_hash(input_string):
5656

5757

5858
# We do not aim to wrap every os-specific exception.
59-
# Here we define only the most common one,
60-
# otherwise caller would need to catch os-specific persistence exceptions.
61-
class PersistenceNotFound(IOError): # Use IOError rather than OSError as base,
59+
# Here we standardize only the most common ones,
60+
# otherwise caller would need to catch os-specific underlying exceptions.
61+
class PersistenceError(IOError): # Use IOError rather than OSError as base,
62+
"""The base exception for persistence."""
6263
# because historically an IOError was bubbled up and expected.
6364
# https://github.com/AzureAD/microsoft-authentication-extensions-for-python/blob/0.2.2/msal_extensions/token_cache.py#L38
6465
# Now we want to maintain backward compatibility even when using Python 2.x
6566
# It makes no difference in Python 3.3+ where IOError is an alias of OSError.
67+
def __init__(self, err_no=None, message=None, location=None): # pylint: disable=useless-super-delegation
68+
super(PersistenceError, self).__init__(err_no, message, location)
69+
70+
71+
class PersistenceNotFound(PersistenceError):
6672
"""This happens when attempting BasePersistence.load() on a non-existent persistence instance"""
6773
def __init__(self, err_no=None, message=None, location=None):
6874
super(PersistenceNotFound, self).__init__(
69-
err_no or errno.ENOENT,
70-
message or "Persistence not found",
71-
location)
75+
err_no=errno.ENOENT,
76+
message=message or "Persistence not found",
77+
location=location)
78+
79+
class PersistenceEncryptionError(PersistenceError):
80+
"""This could be raised by persistence.save()"""
81+
82+
class PersistenceDecryptionError(PersistenceError):
83+
"""This could be raised by persistence.load()"""
7284

7385

7486
class BasePersistence(ABC):
@@ -177,7 +189,13 @@ def __init__(self, location, entropy=''):
177189

178190
def save(self, content):
179191
# type: (str) -> None
180-
data = self._dp_agent.protect(content)
192+
try:
193+
data = self._dp_agent.protect(content)
194+
except OSError as exception:
195+
raise PersistenceEncryptionError(
196+
err_no=getattr(exception, "winerror", None), # Exists in Python 3 on Windows
197+
message="Encryption failed: {}. Consider disable encryption.".format(exception),
198+
)
181199
with os.fdopen(_open(self._location), 'wb+') as handle:
182200
handle.write(data)
183201

@@ -186,7 +204,6 @@ def load(self):
186204
try:
187205
with open(self._location, 'rb') as handle:
188206
data = handle.read()
189-
return self._dp_agent.unprotect(data)
190207
except EnvironmentError as exp: # EnvironmentError in Py 2.7 works across platform
191208
if exp.errno == errno.ENOENT:
192209
raise PersistenceNotFound(
@@ -199,6 +216,17 @@ def load(self):
199216
"DPAPI error likely caused by file content not previously encrypted. "
200217
"App developer should migrate by calling save(plaintext) first.")
201218
raise
219+
try:
220+
return self._dp_agent.unprotect(data)
221+
except OSError as exception:
222+
raise PersistenceDecryptionError(
223+
err_no=getattr(exception, "winerror", None), # Exists in Python 3 on Windows
224+
message="Decryption failed: {}. "
225+
"App developer may consider this guidance: "
226+
"https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/PersistenceDecryptionError" # pylint: disable=line-too-long
227+
.format(exception),
228+
location=self._location,
229+
)
202230

203231

204232
class KeychainPersistence(BasePersistence):

Diff for: msal_extensions/windows.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ def raw(self):
3939
_MEMCPY(blob_buffer, pb_data, cb_data)
4040
return blob_buffer.raw
4141

42+
_err_description = {
43+
# Keys came from real world observation, values came from winerror.h (http://errors (Microsoft internal))
44+
-2146893813: "Key not valid for use in specified state.",
45+
-2146892987: "The requested operation cannot be completed. "
46+
"The computer must be trusted for delegation and "
47+
"the current user account must be configured to allow delegation. "
48+
"See also https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/enable-computer-and-user-accounts-to-be-trusted-for-delegation",
49+
13: "The data is invalid",
50+
}
4251

4352
# This code is modeled from a StackOverflow question, which can be found here:
4453
# https://stackoverflow.com/questions/463832/using-dpapi-with-python
@@ -82,7 +91,7 @@ def protect(self, message):
8291
_LOCAL_FREE(result.pbData)
8392

8493
err_code = _GET_LAST_ERROR()
85-
raise OSError(256, '', '', err_code)
94+
raise OSError(None, _err_description.get(err_code), None, err_code)
8695

8796
def unprotect(self, cipher_text):
8897
# type: (bytes) -> str
@@ -111,4 +120,4 @@ def unprotect(self, cipher_text):
111120
finally:
112121
_LOCAL_FREE(result.pbData)
113122
err_code = _GET_LAST_ERROR()
114-
raise OSError(256, '', '', err_code)
123+
raise OSError(None, _err_description.get(err_code), None, err_code)

Diff for: tests/test_persistence.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@
99
from msal_extensions.persistence import *
1010

1111

12-
is_running_on_travis_ci = bool( # (WTF) What-The-Finding:
12+
def _is_env_var_defined(env_var):
13+
return bool( # (WTF) What-The-Finding:
1314
# The bool(...) is necessary, otherwise skipif(...) would treat "true" as
1415
# string conditions and then raise an undefined "true" exception.
1516
# https://docs.pytest.org/en/latest/historical-notes.html#string-conditions
16-
os.getenv("TRAVIS"))
17+
os.getenv(env_var))
18+
19+
20+
# Note: If you use tox, remember to pass them through via tox.ini
21+
# https://tox.wiki/en/latest/example/basic.html#passing-down-environment-variables
22+
is_running_on_travis_ci = _is_env_var_defined("TRAVIS")
23+
is_running_on_github_ci = _is_env_var_defined("GITHUB_ACTIONS")
1724

1825
@pytest.fixture
1926
def temp_location():
@@ -42,7 +49,13 @@ def test_nonexistent_file_persistence(temp_location):
4249
is_running_on_travis_ci or not sys.platform.startswith('win'),
4350
reason="Requires Windows Desktop")
4451
def test_file_persistence_with_data_protection(temp_location):
45-
_test_persistence_roundtrip(FilePersistenceWithDataProtection(temp_location))
52+
try:
53+
_test_persistence_roundtrip(FilePersistenceWithDataProtection(temp_location))
54+
except PersistenceDecryptionError:
55+
if is_running_on_github_ci or is_running_on_travis_ci:
56+
logging.warning("DPAPI tends to fail on Windows VM. Run this on your desktop to double check.")
57+
else:
58+
raise
4659

4760
@pytest.mark.skipif(
4861
is_running_on_travis_ci or not sys.platform.startswith('win'),

Diff for: tox.ini

+2
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ envlist = py27,py35,py36,py37,py38
55
deps = pytest
66
passenv =
77
TRAVIS
8+
GITHUB_ACTIONS
9+
810
commands =
911
pytest

0 commit comments

Comments
 (0)