Commits

Jason R. Coombs committed 51fdcfc

Initial implementation of prioritized backends

Comments (0)

Files changed (9)

keyring/backend.py

 Keyring implementation support
 """
 
-import sys
-
 from keyring.py25compat import abc
 from keyring import errors
 
     """
     __metaclass__ = KeyringBackendMeta
 
-    @abc.abstractmethod
-    def supported(self):
-        """Return if this keyring supports current environment:
-        -1: not applicable
-         0: suitable
-         1: recommended
+    @abc.abstractproperty
+    def priority(cls):
         """
-        return -1
+        Each backend class must supply a priority, a number (float or integer)
+        indicating the priority of the backend relative to all other backends.
+        The priority need not be static -- it may (and should) vary based
+        attributes of the environment in which is runs (platform, available
+        packages, etc.).
+
+        A higher number indicates a higher priority. The priority should raise
+        a RuntimeError with a message indicating the underlying cause if the
+        backend is not suitable for the current environment.
+
+        As a rule of thumb, a priority between zero but less than one is
+        suitable, but a priority of one or greater is recommended.
+        """
 
     @abc.abstractmethod
     def get_password(self, service, username):

keyring/backends/OS_X.py

 
 from keyring.backend import KeyringBackend
 from keyring.errors import PasswordSetError
+from keyring.util import properties
 
 class Keyring(KeyringBackend):
     """Mac OS X Keychain"""
     password_regex = re.compile("""password:\s*(?:0x(?P<hex>[0-9A-F]+)\s*)?"""
                                 """(?:"(?P<pw>.*)")?""")
 
-    def supported(self):
-        """Recommended for all OSX environment.
+    @properties.ClassProperty
+    @classmethod
+    def priority(cls):
         """
-        return sys.platform == 'darwin' or -1
+        Preferred for all OS X environments.
+        """
+        if not sys.platform == 'darwin':
+            raise RuntimeError("OS X required")
+        return 5
 
     @staticmethod
     def set_password(service, username, password):

keyring/backends/SecretService.py

 import logging
 
+from keyring.util import properties
 from keyring.backend import KeyringBackend
 
+try:
+    import dbus
+except ImportError:
+    pass
+
 log = logging.getLogger(__name__)
 
 class Keyring(KeyringBackend):
     """Secret Service Keyring"""
 
-    def supported(self):
-        try:
-            import dbus
-        except ImportError:
-            return -1
+    @properties.ClassProperty
+    @classmethod
+    def priority(cls):
+        if not 'dbus' in globals():
+            raise RuntimeError("dbus required")
         try:
             bus = dbus.SessionBus()
             bus.get_object('org.freedesktop.secrets',
                 '/org/freedesktop/secrets')
         except dbus.exceptions.DBusException:
-            return -1
-        else:
-            return 1
+            raise RuntimeError("Unable to get dbus resource "
+                "org.freedesktop.secrets")
+        return 5
 
     @staticmethod
     def _str_to_dbus_str(s, strict=False):
 
     @property
     def secret_service(self):
-        import dbus
         bus = dbus.SessionBus()
         service_obj = bus.get_object('org.freedesktop.secrets',
             '/org/freedesktop/secrets')
 
     @property
     def collection(self):
-        import dbus
         bus = dbus.SessionBus()
         collection_obj = bus.get_object(
             'org.freedesktop.secrets',
     def set_password(self, service, username, password):
         """Set password for the username of the service
         """
-        import dbus
         service = self._str_to_dbus_str(service)
         username = self._str_to_dbus_str(username)
         password = self._str_to_dbus_str(password)

keyring/backends/Windows.py

-import os
 import sys
 import base64
 
 import keyring.util.escape
+from keyring.util import properties
 from keyring.backend import KeyringBackend
 from keyring.errors import PasswordDeleteError
 from . import file
 
+try:
+    import pywintypes
+    import win32cred
+except ImportError:
+    pass
+
+try:
+    import winreg
+except ImportError:
+    try:
+        # Python 2 compatibility
+        import _winreg as winreg
+    except ImportError:
+        pass
+
+try:
+    from . import _win_crypto
+except ImportError:
+    pass
+
+def has_pywin32():
+    return 'win32cred' in globals()
+
 class EncryptedKeyring(file.BaseKeyring):
     """
     A File-based keyring secured by Windows Crypto API.
     """
 
+    priority = .8
+    """
+    Preferred over file.EncryptedKeyring but not other, more sophisticated
+    Windows backends.
+    """
+
     filename = 'wincrypto_pass.cfg'
 
-    def __init__(self):
-        super(EncryptedKeyring, self).__init__()
-
-        try:
-            from . import _win_crypto
-            self.crypt_handler = _win_crypto
-        except ImportError:
-            self.crypt_handler = None
-
-    def supported(self):
-        """Recommended when other Windows backends are unavailable
-        """
-        recommended = select_windows_backend()
-        if recommended == None:
-            return -1
-        elif recommended == 'file':
-            return 1
-        else:
-            return 0
-
     def encrypt(self, password):
         """Encrypt the password using the CryptAPI.
         """
-        return self.crypt_handler.encrypt(password)
+        return _win_crypto.encrypt(password)
 
     def decrypt(self, password_encrypted):
         """Decrypt the password using the CryptAPI.
         """
-        return self.crypt_handler.decrypt(password_encrypted)
+        return _win_crypto.decrypt(password_encrypted)
 
 
 class WinVaultKeyring(KeyringBackend):
     in which case the previous password is moved into a compound name:
     {username}@{service}
     """
-    def __init__(self):
-        super(WinVaultKeyring, self).__init__()
-        try:
-            import pywintypes
-            import win32cred
-            self.win32cred = win32cred
-            self.pywintypes = pywintypes
-        except ImportError:
-            self.win32cred = None
 
-    def supported(self):
-        """Default Windows backend, when it is available
+    @properties.ClassProperty
+    @classmethod
+    def priority(cls):
         """
-        recommended = select_windows_backend()
-        if recommended == None:
-            return -1
-        elif recommended == 'cred':
-            return 1
-        else:
-            return 0
+        If available, the preferred backend on Windows.
+        """
+        if not has_pywin32():
+            raise RuntimeError("Requires Windows and pywin32")
+        return 5
 
     @staticmethod
     def _compound_name(username, service):
 
     def _get_password(self, target):
         try:
-            res = self.win32cred.CredRead(
-                Type=self.win32cred.CRED_TYPE_GENERIC,
+            res = win32cred.CredRead(
+                Type=win32cred.CRED_TYPE_GENERIC,
                 TargetName=target,
             )
-        except (self.pywintypes.error,):
+        except pywintypes.error:
             e = sys.exc_info()[1]
             if e.winerror == 1168 and e.funcname == 'CredRead': # not found
                 return None
         self._set_password(service, username, unicode(password))
 
     def _set_password(self, target, username, password):
-        credential = dict(Type=self.win32cred.CRED_TYPE_GENERIC,
+        credential = dict(Type=win32cred.CRED_TYPE_GENERIC,
                           TargetName=target,
                           UserName=username,
                           CredentialBlob=password,
                           Comment="Stored using python-keyring",
-                          Persist=self.win32cred.CRED_PERSIST_ENTERPRISE)
-        self.win32cred.CredWrite(credential, 0)
+                          Persist=win32cred.CRED_PERSIST_ENTERPRISE)
+        win32cred.CredWrite(credential, 0)
 
     def delete_password(self, service, username):
         compound = self._compound_name(username, service)
 
     def _delete_password(self, target):
         self.win32cred.CredDelete(
-            Type=self.win32cred.CRED_TYPE_GENERIC,
+            Type=win32cred.CRED_TYPE_GENERIC,
             TargetName=target,
         )
 
 
 class RegistryKeyring(KeyringBackend):
-    """RegistryKeyring is a keyring which use Windows CryptAPI to encrypt
+    """
+    RegistryKeyring is a keyring which use Windows CryptAPI to encrypt
     the user's passwords and store them under registry keys
     """
-    def __init__(self):
-        super(RegistryKeyring, self).__init__()
 
-        try:
-            from keyring.backends import _win_crypto
-            __import__('_winreg')
-            self.crypt_handler = _win_crypto
-        except ImportError:
-            self.crypt_handler = None
-
-    def supported(self):
-        """Return if this keyring supports current enviroment.
-        -1: not applicable
-         0: suitable
-         1: recommended
+    @properties.ClassProperty
+    @classmethod
+    def priority(self):
         """
-        recommended = select_windows_backend()
-        if recommended == None:
-            return -1
-        elif recommended == 'reg':
-            return 1
-        else:
-            return 0
+        Preferred on Windows when pywin32 isn't installed
+        """
+        if not 'winreg' in globals():
+            raise RuntimeError("Requires Windows")
+        if not '_win_crypto' in globals():
+            raise RuntimeError("Requires ctypes")
+        return 2
 
     def get_password(self, service, username):
         """Get password of the username for the service
         """
-        from _winreg import HKEY_CURRENT_USER, OpenKey, QueryValueEx
         try:
             # fetch the password
             key = r'Software\%s\Keyring' % service
-            hkey = OpenKey(HKEY_CURRENT_USER, key)
-            password_base64 = QueryValueEx(hkey, username)[0]
+            hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key)
+            password_base64 = winreg.QueryValueEx(hkey, username)[0]
             # decode with base64
             password_encrypted = base64.decodestring(password_base64)
             # decrypted the password
-            password = self.crypt_handler.decrypt(password_encrypted)
+            password = _win_crypto.decrypt(password_encrypted)
         except EnvironmentError:
             password = None
         return password
         """Write the password to the registry
         """
         # encrypt the password
-        password_encrypted = self.crypt_handler.encrypt(password)
+        password_encrypted = _win_crypto.encrypt(password)
         # encode with base64
         password_base64 = base64.encodestring(password_encrypted)
 
         # store the password
-        from _winreg import HKEY_CURRENT_USER, CreateKey, SetValueEx, REG_SZ
-        hkey = CreateKey(HKEY_CURRENT_USER, r'Software\%s\Keyring' % service)
-        SetValueEx(hkey, username, 0, REG_SZ, password_base64)
+        key_name = r'Software\%s\Keyring' % service
+        hkey = winreg.CreateKey(winreg.HKEY_CURRENT_USER, key_name)
+        winreg.SetValueEx(hkey, username, 0, winreg.REG_SZ, password_base64)
 
     def delete_password(self, service, username):
         """Delete the password for the username of the service.
         """
-        from _winreg import (KEY_ALL_ACCESS, HKEY_CURRENT_USER, DeleteValue,
-                             OpenKey)
         try:
-            key = r'Software\%s\Keyring' % service
-            hkey = OpenKey(HKEY_CURRENT_USER, key, 0, KEY_ALL_ACCESS)
-            DeleteValue(hkey, username)
-        except WindowsError, e:
+            key_name = r'Software\%s\Keyring' % service
+            hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_name, 0,
+                winreg.KEY_ALL_ACCESS)
+            winreg.DeleteValue(hkey, username)
+        except WindowsError:
+            e = sys.exc_info()[1]
             raise PasswordDeleteError(e)
-
-def select_windows_backend():
-    if os.name != 'nt':
-        return None
-    major, minor, build, platform, text = sys.getwindowsversion()
-    try:
-        __import__('pywintypes')
-        __import__('win32cred')
-        if (major, minor) >= (5, 1):
-            # recommend for windows xp+
-            return 'cred'
-    except ImportError:
-        pass
-    try:
-        __import__('keyring.backends.win32_crypto')
-        __import__('_winreg')
-        if (major, minor) >= (5, 0):
-            # recommend for windows 2k+
-            return 'reg'
-    except ImportError:
-        pass
-    try:
-        __import__('keyring.backends.win32_crypto')
-        return 'file'
-    except ImportError:
-        pass
-    return None

keyring/backends/_win_crypto.py

-#!/usr/bin/python
 
-import sys
-if sys.platform != 'win32':
-    raise ImportError('Windows-only module')
-
-
-from ctypes import Structure, wintypes, POINTER, windll, \
-     WinDLL, c_void_p, WINFUNCTYPE, cast, create_string_buffer, \
-     c_char_p, byref, memmove
+from ctypes import Structure, POINTER, c_void_p, cast, create_string_buffer, \
+    c_char_p, byref, memmove
+from ctypes import windll, WinDLL, WINFUNCTYPE, wintypes
 
 from keyring.util.escape import u
 

keyring/backends/file.py

 class PlaintextKeyring(BaseKeyring):
     """Simple File Keyring with no encryption"""
 
+    priority = .5
+    "Applicable for all platforms, but not recommended"
+
     filename = 'keyring_pass.cfg'
 
     def encrypt(self, password):
         """
         return password_encrypted
 
-    def supported(self):
-        """Applicable for all platforms, but do not recommend.
-        """
-        return 0
-
 class EncryptedKeyring(BaseKeyring):
     """PyCrypto File Keyring"""
 
 
     filename = 'crypted_pass.cfg'
 
-    def supported(self):
-        """Applicable for all platforms, but not recommend"
-        """
+    @properties.ClassProperty
+    @classmethod
+    def priority(self):
+        "Applicable for all platforms, but not recommended."
         try:
             __import__('Crypto.Cipher.AES')
             __import__('Crypto.Protocol.KDF')
             __import__('Crypto.Random')
-            if not json:
-                raise AssertionError("JSON implementation needed (install "
-                    "simplejson)")
-            status = 0
-        except (ImportError, AssertionError):
-            status = -1
-        return status
+        except ImportError:
+            raise RuntimeError("PyCrypto required")
+        if not json:
+            raise RuntimeError("JSON implementation such as simplejson "
+                "required.")
+        return .6
 
     @properties.NonDataProperty
     def keyring_key(self):

keyring/backends/kwallet.py

 from keyring.backend import KeyringBackend
 from keyring.errors import PasswordDeleteError
 from keyring.errors import PasswordSetError
+from keyring.util import properties
 
 try:
     from PyKDE4.kdeui import KWallet
 class Keyring(KeyringBackend):
     """KDE KWallet"""
 
-    def supported(self):
-        if kwallet_support and 'KDE_SESSION_UID' in os.environ:
-            return 1
-        elif kwallet_support:
+    @properties.ClassProperty
+    @classmethod
+    def priority(cls):
+        if 'KWallet' not in globals():
+            raise RuntimeError("KDE libraries not available")
+        if 'KDE_SESSION_ID' not in os.environ:
             return 0
-        else:
-            return -1
+        return 5
 
     def get_password(self, service, username):
         """Get password of the username for the service

keyring/backends/multi.py

         self._keyring = keyring
         self._max_password_size = max_password_size
 
-    def supported(self):
-        """Return if this keyring supports current environment:
-        -1: not applicable
-         0: suitable
-         1: recommended
-        """
-        return self._keyring.supported()
+    @properties.ClassProperty
+    @classmethod
+    def priority(cls):
+        return 0
 
     def get_password(self, service, username):
         """Get password of the username for the service

keyring/backends/pyfs.py

 from keyring.backend import KeyringBackend, NullCrypter
 from . import keyczar
 
+try:
+    import fs.opener
+    import fs.errors
+    import fs.path
+    import fs.remote
+except ImportError:
+    pass
+
 class BasicKeyring(KeyringBackend):
     """BasicKeyring is a Pyfilesystem-based implementation of
     keyring.
     def _open(self, mode='rb'):
         """Open the password file in the specified mode
         """
-        import fs.opener
-        import fs.errors
-        import fs.path
-        import fs.remote
         open_file = None
         writeable = 'w' in mode or 'a' in mode or '+' in mode
         try:
         self.config.write(config_file)
         config_file.close()
 
-    def supported(self):
-        """Applicable when Pyfilesystem installed, but do not recommend.
-        """
-        try:
-            from fs.opener import fsopen
-            return 0
-        except ImportError:
-            return -1
+    @properties.ClassProperty
+    @classmethod
+    def priority(cls):
+        if not 'fs' in globals():
+            raise RuntimeError("pyfs required")
+        return 2
 
 class PlaintextKeyring(BasicKeyring):
     """Unencrypted Pyfilesystem Keyring
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.