Skip to content

Commit

Permalink
Add support for a scheme property in SecretService backend. Set to 'K…
Browse files Browse the repository at this point in the history
…eypassXC' to use the username/system convention from KeypassXC, either with KEYRING_PROPERTY_SCHEME or keyring.with_properties(scheme='KeypassXC'). Fixes #448.
  • Loading branch information
jaraco committed Aug 2, 2022
1 parent ba4ce89 commit 870b5f3
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 13 deletions.
6 changes: 6 additions & 0 deletions keyring/backend.py
Expand Up @@ -6,6 +6,7 @@
import abc
import logging
import operator
import copy

from typing import Optional

Expand Down Expand Up @@ -151,6 +152,11 @@ def parse(item):
for name, value in props:
setattr(self, name, value)

def with_properties(self, **kwargs):
alt = copy.copy(self)
vars(alt).update(kwargs)
return alt


class Crypter:
"""Base class providing encryption and decryption"""
Expand Down
41 changes: 28 additions & 13 deletions keyring/backends/SecretService.py
Expand Up @@ -27,6 +27,12 @@ class Keyring(KeyringBackend):
"""Secret Service Keyring"""

appid = 'Python keyring library'
scheme = 'default'

schemes = dict(
default=dict(username='username', service='service'),
KeypassXC=dict(username='UserName', service='Title'),
)

@properties.ClassProperty
@classmethod
Expand Down Expand Up @@ -73,23 +79,35 @@ def unlock(self, item):
if item.is_locked(): # User dismissed the prompt
raise KeyringLocked('Failed to unlock the item!')

def _query(self, service, username):
scheme = self.schemes[self.scheme]
return (
{
scheme['username']: username,
scheme['service']: service,
}
if username
else {
scheme['service']: service,
}
)

def get_password(self, service, username):
"""Get password of the username for the service"""
collection = self.get_preferred_collection()
with closing(collection.connection):
items = collection.search_items({"username": username, "service": service})
items = collection.search_items(self._query(service, username))
for item in items:
self.unlock(item)
return item.get_secret().decode('utf-8')

def set_password(self, service, username, password):
"""Set password for the username of the service"""
collection = self.get_preferred_collection()
attributes = {
"application": self.appid,
"service": service,
"username": username,
}
attributes = dict(
self._query(service, username),
application=self.appid,
)
label = "Password for '{}' on '{}'".format(username, service)
with closing(collection.connection):
collection.create_item(label, attributes, password, replace=True)
Expand All @@ -98,7 +116,7 @@ def delete_password(self, service, username):
"""Delete the stored password (only the first one)"""
collection = self.get_preferred_collection()
with closing(collection.connection):
items = collection.search_items({"username": username, "service": service})
items = collection.search_items(self._query(service, username))
for item in items:
return item.delete()
raise PasswordDeleteError("No such password!")
Expand All @@ -111,16 +129,13 @@ def get_credential(self, service, username):
and return a SimpleCredential containing the username and password
Otherwise, it will return the first username and password combo that it finds.
"""

query = {"service": service}
if username:
query["username"] = username

scheme = self.schemes[self.scheme]
query = self._query(service, username)
collection = self.get_preferred_collection()

with closing(collection.connection):
items = collection.search_items(query)
for item in items:
self.unlock(item)
username = item.get_attributes().get("username")
username = item.get_attributes().get(scheme['username'])
return SimpleCredential(username, item.get_secret().decode('utf-8'))

0 comments on commit 870b5f3

Please sign in to comment.