Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions keyring/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

by_priority = operator.attrgetter('priority')
_limit: typing.Callable[[KeyringBackend], bool] | None = None
_QT = typing.TypeVar("_QT")


class KeyringBackendMeta(abc.ABCMeta):
Expand Down Expand Up @@ -284,8 +285,8 @@ class SchemeSelectable:
)

def _query(
self, service: str, username: str | None = None, **base: typing.Any
) -> dict[str, str]:
self, service: _QT, username: _QT | None = None, **base: typing.Any
) -> dict[str, _QT]:
scheme = self.schemes[self.scheme]
return dict(
{
Expand Down
49 changes: 32 additions & 17 deletions keyring/backends/libsecret.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
import typing

from .. import backend
from ..backend import KeyringBackend
from ..compat import properties
from ..credentials import SimpleCredential
from ..credentials import Credential, SimpleCredential
from ..errors import (
KeyringLocked,
PasswordDeleteError,
Expand All @@ -20,7 +21,12 @@

available = True
except (AttributeError, ImportError, ValueError):
pass
if typing.TYPE_CHECKING:
from gi.repository import (
Gio, # noqa: TC004
GLib, # noqa: TC004
Secret, # noqa: TC004
)

log = logging.getLogger(__name__)

Expand All @@ -31,7 +37,7 @@ class Keyring(backend.SchemeSelectable, KeyringBackend):
appid = 'Python keyring library'

@property
def schema(self):
def schema(self) -> Secret.Schema:
return Secret.Schema.new(
"org.freedesktop.Secret.Generic",
Secret.SchemaFlags.NONE,
Expand All @@ -43,7 +49,7 @@ def schema(self):
)

@properties.NonDataProperty
def collection(self):
def collection(self) -> str:
return Secret.COLLECTION_DEFAULT

@properties.classproperty
Expand All @@ -59,7 +65,14 @@ def priority(cls) -> float:

return 4.8

def get_password(self, service, username):
def _get_text_from_item(self, item: Secret.Retrievable) -> str:
value = item.retrieve_secret_sync()
assert value is not None
text = value.get_text()
assert text is not None
return text

def get_password(self, service: str, username: str) -> str | None:
"""Get password of the username for the service"""
attributes = self._query(service, username, application=self.appid)
try:
Expand All @@ -73,14 +86,16 @@ def get_password(self, service, username):
raise
for item in items:
try:
return item.retrieve_secret_sync().get_text()
return self._get_text_from_item(item)
except GLib.Error as error:
quark = GLib.quark_try_string('secret-error')
if error.matches(quark, Secret.Error.IS_LOCKED):
raise KeyringLocked('Failed to unlock the item!') from error
raise

def set_password(self, service, username, password):
return None

def set_password(self, service: str, username: str, password: str) -> None:
"""Set password for the username of the service"""
attributes = self._query(service, username, application=self.appid)
label = f"Password for '{username}' on '{service}'"
Expand All @@ -99,7 +114,7 @@ def set_password(self, service, username, password):
if not stored:
raise PasswordSetError("Failed to store password!")

def delete_password(self, service, username):
def delete_password(self, service: str, username: str) -> None:
"""Delete the stored password (only the first one)"""
attributes = self._query(service, username, application=self.appid)
try:
Expand All @@ -113,18 +128,16 @@ def delete_password(self, service, username):
raise
for item in items:
try:
removed = Secret.password_clear_sync(
self.schema, item.get_attributes(), None
)
Secret.password_clear_sync(self.schema, item.get_attributes(), None)
except GLib.Error as error:
quark = GLib.quark_try_string('secret-error')
if error.matches(quark, Secret.Error.IS_LOCKED):
raise KeyringLocked('Failed to unlock the item!') from error
raise
return removed

raise PasswordDeleteError("No such password!")

def get_credential(self, service, username):
def get_credential(self, service: str, username: str | None) -> Credential | None:
"""Get the first username and password for a service.
Return a Credential instance

Expand All @@ -133,6 +146,8 @@ def get_credential(self, service, username):
Otherwise, it will return the first username and password combo that it finds.
"""
query = self._query(service, username)
scheme = self.schemes[self.scheme]

try:
items = Secret.password_search_sync(
self.schema, query, Secret.SearchFlags.UNLOCK, None
Expand All @@ -143,13 +158,13 @@ def get_credential(self, service, username):
raise KeyringLocked('Failed to unlock the item!') from error
raise
for item in items:
username = item.get_attributes().get("username")
username = item.get_attributes()[scheme["username"]]
try:
return SimpleCredential(
username, item.retrieve_secret_sync().get_text()
)
return SimpleCredential(username, self._get_text_from_item(item))
except GLib.Error as error:
quark = GLib.quark_try_string('secret-error')
if error.matches(quark, Secret.Error.IS_LOCKED):
raise KeyringLocked('Failed to unlock the item!') from error
raise

return None
Loading