import logging logger = logging.getLogger(__name__) try: import dbus except ImportError: dbus = None # XXX: Hack to workaround introspection bug caused by gnome-keyring, should be # fixed by version 3.5 per: # https://git.gnome.org/browse/gnome-keyring/commit/?id=5dccbe88eb94eea9934e2b7 if dbus: EMPTY_STRING = dbus.String("", variant_level=1) else: EMPTY_STRING = "" FETCH_ERROR = ( "Fetching passwords from your keyring failed. Any passwords " "stored in the keyring will not be available." ) def fetch(): if not dbus: logger.debug("%s (dbus not installed)", FETCH_ERROR) return [] try: bus = dbus.SessionBus() except dbus.exceptions.DBusException as e: logger.debug("%s (%s)", FETCH_ERROR, e) return [] if not bus.name_has_owner("org.freedesktop.secrets"): logger.debug( "%s (org.freedesktop.secrets service not running)", FETCH_ERROR ) return [] service = _service(bus) session = service.OpenSession("plain", EMPTY_STRING)[1] items, locked = service.SearchItems({"service": "mopidy"}) if not locked and not items: return [] if locked: # There is a chance we can unlock without prompting the users... items, prompt = service.Unlock(locked) if prompt != "/": _prompt(bus, prompt).Dismiss() logger.debug("%s (Keyring is locked)", FETCH_ERROR) return [] result = [] secrets = service.GetSecrets(items, session, byte_arrays=True) for item_path, values in secrets.items(): session_path, parameters, value, content_type = values attrs = _item_attributes(bus, item_path) result.append((attrs["section"], attrs["key"], bytes(value))) return result def set(section, key, value): """Store a secret config value for a given section/key. Indicates if storage failed or succeeded. """ if not dbus: logger.debug( "Saving %s/%s to keyring failed. (dbus not installed)", section, key ) return False try: bus = dbus.SessionBus() except dbus.exceptions.DBusException as e: logger.debug("Saving %s/%s to keyring failed. (%s)", section, key, e) return False if not bus.name_has_owner("org.freedesktop.secrets"): logger.debug( "Saving %s/%s to keyring failed. " "(org.freedesktop.secrets service not running)", section, key, ) return False service = _service(bus) collection = _collection(bus) if not collection: return False if isinstance(value, str): value = value.encode() session = service.OpenSession("plain", EMPTY_STRING)[1] secret = dbus.Struct( (session, "", dbus.ByteArray(value), "plain/text; charset=utf8") ) label = f"mopidy: {section}/{key}" attributes = {"service": "mopidy", "section": section, "key": key} properties = { "org.freedesktop.Secret.Item.Label": label, "org.freedesktop.Secret.Item.Attributes": attributes, } try: item, prompt = collection.CreateItem(properties, secret, True) except dbus.exceptions.DBusException as e: # TODO: catch IsLocked errors etc. logger.debug("Saving %s/%s to keyring failed. (%s)", section, key, e) return False if prompt == "/": return True _prompt(bus, prompt).Dismiss() logger.debug( "Saving secret %s/%s failed. (Keyring is locked)", section, key ) return False def _service(bus): return _interface( bus, "/org/freedesktop/secrets", "org.freedesktop.Secret.Service" ) # NOTE: depending on versions and setup 'default' might not exists, so try and # use it but fall back to the 'login' collection, and finally the 'session' one # if all else fails. We should probably create a keyring/collection setting # that allows users to set this so they have control over where their secrets # get stored. def _collection(bus): for name in "aliases/default", "collection/login", "collection/session": path = "/org/freedesktop/secrets/" + name if _collection_exists(bus, path): break else: return None return _interface(bus, path, "org.freedesktop.Secret.Collection") # NOTE: Hack to probe if a given collection actually exists. Needed to work # around an introspection bug in setting passwords for non-existant aliases. def _collection_exists(bus, path): try: item = _interface(bus, path, "org.freedesktop.DBus.Properties") item.Get("org.freedesktop.Secret.Collection", "Label") return True except dbus.exceptions.DBusException: return False # NOTE: We could call prompt.Prompt('') to unlock the keyring when it is not # '/', but we would then also have to arrange to setup signals to wait until # this has been completed. So for now we just dismiss the prompt and expect # keyrings to be unlocked. def _prompt(bus, path): return _interface(bus, path, "Prompt") def _item_attributes(bus, path): item = _interface(bus, path, "org.freedesktop.DBus.Properties") result = item.Get("org.freedesktop.Secret.Item", "Attributes") return {bytes(k): bytes(v) for k, v in result.items()} def _interface(bus, path, interface): obj = bus.get_object("org.freedesktop.secrets", path) return dbus.Interface(obj, interface)