99import  abc 
1010import  os 
1111import  errno 
12+ import  hashlib 
1213import  logging 
1314import  sys 
1415try :
@@ -50,21 +51,52 @@ def _mkdir_p(path):
5051        else :
5152            raise 
5253
54+ def  _auto_hash (input_string ):
55+     return  hashlib .sha256 (input_string .encode ('utf-8' )).hexdigest ()
56+ 
5357
5458# We do not aim to wrap every os-specific exception. 
55- # Here we define only the most common one, 
56- # otherwise caller would need to catch os-specific persistence exceptions. 
57- 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.""" 
5863        # because historically an IOError was bubbled up and expected. 
5964        # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/blob/0.2.2/msal_extensions/token_cache.py#L38 
6065        # Now we want to maintain backward compatibility even when using Python 2.x 
6166        # 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 ):
6272    """This happens when attempting BasePersistence.load() on a non-existent persistence instance""" 
6373    def  __init__ (self , err_no = None , message = None , location = None ):
6474        super (PersistenceNotFound , self ).__init__ (
65-             err_no  or  errno .ENOENT ,
66-             message  or  "Persistence not found" ,
67-             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()""" 
84+ 
85+ 
86+ def  build_encrypted_persistence (location ):
87+     """Build a suitable encrypted persistence instance based your current OS. 
88+ 
89+     If you do not need encryption, then simply use ``FilePersistence`` constructor. 
90+     """ 
91+     # Does not (yet?) support fallback_to_plaintext flag, 
92+     # because the persistence on Windows and macOS do not support built-in trial_run(). 
93+     if  sys .platform .startswith ('win' ):
94+         return  FilePersistenceWithDataProtection (location )
95+     if  sys .platform .startswith ('darwin' ):
96+         return  KeychainPersistence (location )
97+     if  sys .platform .startswith ('linux' ):
98+         return  LibsecretPersistence (location )
99+     raise  RuntimeError ("Unsupported platform: {}" .format (sys .platform ))  # pylint: disable=consider-using-f-string 
68100
69101
70102class  BasePersistence (ABC ):
@@ -101,6 +133,11 @@ def get_location(self):
101133        raise  NotImplementedError 
102134
103135
136+ def  _open (location ):
137+     return  os .open (location , os .O_RDWR  |  os .O_CREAT  |  os .O_TRUNC , 0o600 )
138+         # The 600 seems no-op on NTFS/Windows, and that is fine 
139+ 
140+ 
104141class  FilePersistence (BasePersistence ):
105142    """A generic persistence, storing data in a plain-text file""" 
106143
@@ -113,7 +150,7 @@ def __init__(self, location):
113150    def  save (self , content ):
114151        # type: (str) -> None 
115152        """Save the content into this persistence""" 
116-         with  open ( self ._location , 'w+' ) as  handle :   # pylint: disable=unspecified-encoding 
153+         with  os . fdopen ( _open ( self ._location ) , 'w+' ) as  handle :
117154            handle .write (content )
118155
119156    def  load (self ):
@@ -168,16 +205,21 @@ def __init__(self, location, entropy=''):
168205
169206    def  save (self , content ):
170207        # type: (str) -> None 
171-         data  =  self ._dp_agent .protect (content )
172-         with  open (self ._location , 'wb+' ) as  handle :
208+         try :
209+             data  =  self ._dp_agent .protect (content )
210+         except  OSError  as  exception :
211+             raise  PersistenceEncryptionError (
212+                 err_no = getattr (exception , "winerror" , None ),  # Exists in Python 3 on Windows 
213+                 message = "Encryption failed: {}. Consider disable encryption." .format (exception ),
214+                 )
215+         with  os .fdopen (_open (self ._location ), 'wb+' ) as  handle :
173216            handle .write (data )
174217
175218    def  load (self ):
176219        # type: () -> str 
177220        try :
178221            with  open (self ._location , 'rb' ) as  handle :
179222                data  =  handle .read ()
180-             return  self ._dp_agent .unprotect (data )
181223        except  EnvironmentError  as  exp :  # EnvironmentError in Py 2.7 works across platform 
182224            if  exp .errno  ==  errno .ENOENT :
183225                raise  PersistenceNotFound (
@@ -190,26 +232,36 @@ def load(self):
190232                "DPAPI error likely caused by file content not previously encrypted. " 
191233                "App developer should migrate by calling save(plaintext) first." )
192234            raise 
235+         try :
236+             return  self ._dp_agent .unprotect (data )
237+         except  OSError  as  exception :
238+             raise  PersistenceDecryptionError (
239+                 err_no = getattr (exception , "winerror" , None ),  # Exists in Python 3 on Windows 
240+                 message = "Decryption failed: {}. " 
241+                     "App developer may consider this guidance: " 
242+                     "https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/PersistenceDecryptionError"   # pylint: disable=line-too-long 
243+                     .format (exception ),
244+                 location = self ._location ,
245+                 )
193246
194247
195248class  KeychainPersistence (BasePersistence ):
196249    """A generic persistence with data stored in, 
197250    and protected by native Keychain libraries on OSX""" 
198251    is_encrypted  =  True 
199252
200-     def  __init__ (self , signal_location , service_name , account_name ):
253+     def  __init__ (self , signal_location , service_name = None , account_name = None ):
201254        """Initialization could fail due to unsatisfied dependency. 
202255
203256        :param signal_location: See :func:`persistence.LibsecretPersistence.__init__` 
204257        """ 
205-         if  not  (service_name  and  account_name ):  # It would hang on OSX 
206-             raise  ValueError ("service_name and account_name are required" )
207258        from  .osx  import  Keychain , KeychainError   # pylint: disable=import-outside-toplevel 
208259        self ._file_persistence  =  FilePersistence (signal_location )  # Favor composition 
209260        self ._Keychain  =  Keychain   # pylint: disable=invalid-name 
210261        self ._KeychainError  =  KeychainError   # pylint: disable=invalid-name 
211-         self ._service_name  =  service_name 
212-         self ._account_name  =  account_name 
262+         default_service_name  =  "msal-extensions"   # This is also our package name 
263+         self ._service_name  =  service_name  or  default_service_name 
264+         self ._account_name  =  account_name  or  _auto_hash (signal_location )
213265
214266    def  save (self , content ):
215267        with  self ._Keychain () as  locker :
@@ -247,7 +299,7 @@ class LibsecretPersistence(BasePersistence):
247299    and protected by native libsecret libraries on Linux""" 
248300    is_encrypted  =  True 
249301
250-     def  __init__ (self , signal_location , schema_name , attributes , ** kwargs ):
302+     def  __init__ (self , signal_location , schema_name = None , attributes = None , ** kwargs ):
251303        """Initialization could fail due to unsatisfied dependency. 
252304
253305        :param string signal_location: 
@@ -262,7 +314,8 @@ def __init__(self, signal_location, schema_name, attributes, **kwargs):
262314        from  .libsecret  import  (  # This uncertain import is deferred till runtime 
263315            LibSecretAgent , trial_run )
264316        trial_run ()
265-         self ._agent  =  LibSecretAgent (schema_name , attributes , ** kwargs )
317+         self ._agent  =  LibSecretAgent (
318+             schema_name  or  _auto_hash (signal_location ), attributes  or  {}, ** kwargs )
266319        self ._file_persistence  =  FilePersistence (signal_location )  # Favor composition 
267320
268321    def  save (self , content ):
0 commit comments