mirror of
https://github.com/EvolutionAPI/evolution-client-python.git
synced 2026-02-04 22:06:22 -06:00
initial commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import logging
|
||||
from contextlib import closing
|
||||
|
||||
from jaraco.context import ExceptionTrap
|
||||
|
||||
from .. import backend
|
||||
from ..backend import KeyringBackend
|
||||
from ..compat import properties
|
||||
from ..credentials import SimpleCredential
|
||||
from ..errors import (
|
||||
InitError,
|
||||
KeyringLocked,
|
||||
PasswordDeleteError,
|
||||
)
|
||||
|
||||
try:
|
||||
import secretstorage
|
||||
import secretstorage.exceptions as exceptions
|
||||
except ImportError:
|
||||
pass
|
||||
except AttributeError:
|
||||
# See https://github.com/jaraco/keyring/issues/296
|
||||
pass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Keyring(backend.SchemeSelectable, KeyringBackend):
|
||||
"""Secret Service Keyring"""
|
||||
|
||||
appid = 'Python keyring library'
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls) -> float:
|
||||
with ExceptionTrap() as exc:
|
||||
secretstorage.__name__ # noqa: B018
|
||||
if exc:
|
||||
raise RuntimeError("SecretStorage required")
|
||||
if secretstorage.__version_tuple__ < (3, 2):
|
||||
raise RuntimeError("SecretStorage 3.2 or newer required")
|
||||
try:
|
||||
with closing(secretstorage.dbus_init()) as connection:
|
||||
if not secretstorage.check_service_availability(connection):
|
||||
raise RuntimeError(
|
||||
"The Secret Service daemon is neither running nor "
|
||||
"activatable through D-Bus"
|
||||
)
|
||||
except exceptions.SecretStorageException as e:
|
||||
raise RuntimeError(f"Unable to initialize SecretService: {e}") from e
|
||||
return 5
|
||||
|
||||
def get_preferred_collection(self):
|
||||
"""If self.preferred_collection contains a D-Bus path,
|
||||
the collection at that address is returned. Otherwise,
|
||||
the default collection is returned.
|
||||
"""
|
||||
bus = secretstorage.dbus_init()
|
||||
try:
|
||||
if hasattr(self, 'preferred_collection'):
|
||||
collection = secretstorage.Collection(bus, self.preferred_collection)
|
||||
else:
|
||||
collection = secretstorage.get_default_collection(bus)
|
||||
except exceptions.SecretStorageException as e:
|
||||
raise InitError(f"Failed to create the collection: {e}.") from e
|
||||
if collection.is_locked():
|
||||
collection.unlock()
|
||||
if collection.is_locked(): # User dismissed the prompt
|
||||
raise KeyringLocked("Failed to unlock the collection!")
|
||||
return collection
|
||||
|
||||
def unlock(self, item):
|
||||
if hasattr(item, 'unlock'):
|
||||
item.unlock()
|
||||
if item.is_locked(): # User dismissed the prompt
|
||||
raise KeyringLocked('Failed to unlock the item!')
|
||||
|
||||
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(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 = self._query(service, username, application=self.appid)
|
||||
label = f"Password for '{username}' on '{service}'"
|
||||
with closing(collection.connection):
|
||||
collection.create_item(label, attributes, password, replace=True)
|
||||
|
||||
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(self._query(service, username))
|
||||
for item in items:
|
||||
return item.delete()
|
||||
raise PasswordDeleteError("No such password!")
|
||||
|
||||
def get_credential(self, service, username):
|
||||
"""Gets the first username and password for a service.
|
||||
Returns a Credential instance
|
||||
|
||||
The username can be omitted, but if there is one, it will use get_password
|
||||
and return a SimpleCredential containing the username and password
|
||||
Otherwise, it will return the first username and password combo that it finds.
|
||||
"""
|
||||
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(scheme['username'])
|
||||
return SimpleCredential(username, item.get_secret().decode('utf-8'))
|
||||
@@ -0,0 +1,168 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from jaraco.context import ExceptionTrap
|
||||
|
||||
from ..backend import KeyringBackend
|
||||
from ..compat import properties
|
||||
from ..credentials import SimpleCredential
|
||||
from ..errors import PasswordDeleteError
|
||||
|
||||
with ExceptionTrap() as missing_deps:
|
||||
try:
|
||||
# prefer pywin32-ctypes
|
||||
from win32ctypes.pywin32 import pywintypes, win32cred
|
||||
|
||||
# force demand import to raise ImportError
|
||||
win32cred.__name__ # noqa: B018
|
||||
except ImportError:
|
||||
# fallback to pywin32
|
||||
import pywintypes
|
||||
import win32cred
|
||||
|
||||
# force demand import to raise ImportError
|
||||
win32cred.__name__ # noqa: B018
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Persistence:
|
||||
def __get__(self, keyring, type=None):
|
||||
return getattr(keyring, '_persist', win32cred.CRED_PERSIST_ENTERPRISE)
|
||||
|
||||
def __set__(self, keyring, value):
|
||||
"""
|
||||
Set the persistence value on the Keyring. Value may be
|
||||
one of the win32cred.CRED_PERSIST_* constants or a
|
||||
string representing one of those constants. For example,
|
||||
'local machine' or 'session'.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
attr = 'CRED_PERSIST_' + value.replace(' ', '_').upper()
|
||||
value = getattr(win32cred, attr)
|
||||
keyring._persist = value
|
||||
|
||||
|
||||
class DecodingCredential(dict):
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
Attempt to decode the credential blob as UTF-16 then UTF-8.
|
||||
"""
|
||||
cred = self['CredentialBlob']
|
||||
try:
|
||||
return cred.decode('utf-16')
|
||||
except UnicodeDecodeError:
|
||||
decoded_cred_utf8 = cred.decode('utf-8')
|
||||
log.warning(
|
||||
"Retrieved a UTF-8 encoded credential. Please be aware that "
|
||||
"this library only writes credentials in UTF-16."
|
||||
)
|
||||
return decoded_cred_utf8
|
||||
|
||||
|
||||
class WinVaultKeyring(KeyringBackend):
|
||||
"""
|
||||
WinVaultKeyring stores encrypted passwords using the Windows Credential
|
||||
Manager.
|
||||
|
||||
Requires pywin32
|
||||
|
||||
This backend does some gymnastics to simulate multi-user support,
|
||||
which WinVault doesn't support natively. See
|
||||
https://github.com/jaraco/keyring/issues/47#issuecomment-75763152
|
||||
for details on the implementation, but here's the gist:
|
||||
|
||||
Passwords are stored under the service name unless there is a collision
|
||||
(another password with the same service name but different user name),
|
||||
in which case the previous password is moved into a compound name:
|
||||
{username}@{service}
|
||||
"""
|
||||
|
||||
persist = Persistence()
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls) -> float:
|
||||
"""
|
||||
If available, the preferred backend on Windows.
|
||||
"""
|
||||
if missing_deps:
|
||||
raise RuntimeError("Requires Windows and pywin32")
|
||||
return 5
|
||||
|
||||
@staticmethod
|
||||
def _compound_name(username, service):
|
||||
return f'{username}@{service}'
|
||||
|
||||
def get_password(self, service, username):
|
||||
res = self._resolve_credential(service, username)
|
||||
return res and res.value
|
||||
|
||||
def _resolve_credential(
|
||||
self, service: str, username: str | None
|
||||
) -> DecodingCredential | None:
|
||||
# first attempt to get the password under the service name
|
||||
res = self._read_credential(service)
|
||||
if not res or username and res['UserName'] != username:
|
||||
# It wasn't found so attempt to get it with the compound name
|
||||
res = self._read_credential(self._compound_name(username, service))
|
||||
return res
|
||||
|
||||
def _read_credential(self, target):
|
||||
try:
|
||||
res = win32cred.CredRead(
|
||||
Type=win32cred.CRED_TYPE_GENERIC, TargetName=target
|
||||
)
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1168 and e.funcname == 'CredRead': # not found
|
||||
return None
|
||||
raise
|
||||
return DecodingCredential(res)
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
existing_pw = self._read_credential(service)
|
||||
if existing_pw:
|
||||
# resave the existing password using a compound target
|
||||
existing_username = existing_pw['UserName']
|
||||
target = self._compound_name(existing_username, service)
|
||||
self._set_password(
|
||||
target,
|
||||
existing_username,
|
||||
existing_pw.value,
|
||||
)
|
||||
self._set_password(service, username, str(password))
|
||||
|
||||
def _set_password(self, target, username, password):
|
||||
credential = dict(
|
||||
Type=win32cred.CRED_TYPE_GENERIC,
|
||||
TargetName=target,
|
||||
UserName=username,
|
||||
CredentialBlob=password,
|
||||
Comment="Stored using python-keyring",
|
||||
Persist=self.persist,
|
||||
)
|
||||
win32cred.CredWrite(credential, 0)
|
||||
|
||||
def delete_password(self, service, username):
|
||||
compound = self._compound_name(username, service)
|
||||
deleted = False
|
||||
for target in service, compound:
|
||||
existing_pw = self._read_credential(target)
|
||||
if existing_pw and existing_pw['UserName'] == username:
|
||||
deleted = True
|
||||
self._delete_password(target)
|
||||
if not deleted:
|
||||
raise PasswordDeleteError(service)
|
||||
|
||||
def _delete_password(self, target):
|
||||
try:
|
||||
win32cred.CredDelete(Type=win32cred.CRED_TYPE_GENERIC, TargetName=target)
|
||||
except pywintypes.error as e:
|
||||
if e.winerror == 1168 and e.funcname == 'CredDelete': # not found
|
||||
return
|
||||
raise
|
||||
|
||||
def get_credential(self, service, username):
|
||||
res = self._resolve_credential(service, username)
|
||||
return res and SimpleCredential(res['UserName'], res.value)
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
Keyring Chainer - iterates over other viable backends to
|
||||
discover passwords in each.
|
||||
"""
|
||||
|
||||
from .. import backend
|
||||
from ..compat import properties
|
||||
from . import fail
|
||||
|
||||
|
||||
class ChainerBackend(backend.KeyringBackend):
|
||||
"""
|
||||
>>> ChainerBackend()
|
||||
<keyring.backends.chainer.ChainerBackend object at ...>
|
||||
"""
|
||||
|
||||
# override viability as 'priority' cannot be determined
|
||||
# until other backends have been constructed
|
||||
viable = True
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls) -> float:
|
||||
"""
|
||||
If there are backends to chain, high priority
|
||||
Otherwise very low priority since our operation when empty
|
||||
is the same as null.
|
||||
"""
|
||||
return 10 if len(cls.backends) > 1 else (fail.Keyring.priority - 1)
|
||||
|
||||
@properties.classproperty
|
||||
def backends(cls):
|
||||
"""
|
||||
Discover all keyrings for chaining.
|
||||
"""
|
||||
|
||||
def allow(keyring):
|
||||
limit = backend._limit or bool
|
||||
return (
|
||||
not isinstance(keyring, ChainerBackend)
|
||||
and limit(keyring)
|
||||
and keyring.priority > 0
|
||||
)
|
||||
|
||||
allowed = filter(allow, backend.get_all_keyring())
|
||||
return sorted(allowed, key=backend.by_priority, reverse=True)
|
||||
|
||||
def get_password(self, service, username):
|
||||
for keyring in self.backends:
|
||||
password = keyring.get_password(service, username)
|
||||
if password is not None:
|
||||
return password
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
for keyring in self.backends:
|
||||
try:
|
||||
return keyring.set_password(service, username, password)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
def delete_password(self, service, username):
|
||||
for keyring in self.backends:
|
||||
try:
|
||||
return keyring.delete_password(service, username)
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
def get_credential(self, service, username):
|
||||
for keyring in self.backends:
|
||||
credential = keyring.get_credential(service, username)
|
||||
if credential is not None:
|
||||
return credential
|
||||
@@ -0,0 +1,30 @@
|
||||
from ..backend import KeyringBackend
|
||||
from ..compat import properties
|
||||
from ..errors import NoKeyringError
|
||||
|
||||
|
||||
class Keyring(KeyringBackend):
|
||||
"""
|
||||
Keyring that raises error on every operation.
|
||||
|
||||
>>> kr = Keyring()
|
||||
>>> kr.get_password('svc', 'user')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
keyring.errors.NoKeyringError: ...No recommended backend...
|
||||
"""
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls) -> float:
|
||||
return 0
|
||||
|
||||
def get_password(self, service, username, password=None):
|
||||
msg = (
|
||||
"No recommended backend was available. Install a recommended 3rd "
|
||||
"party backend package; or, install the keyrings.alt package if "
|
||||
"you want to use the non-recommended backends. See "
|
||||
"https://pypi.org/project/keyring for details."
|
||||
)
|
||||
raise NoKeyringError(msg)
|
||||
|
||||
set_password = delete_password = get_password
|
||||
@@ -0,0 +1,164 @@
|
||||
import contextlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ..backend import KeyringBackend
|
||||
from ..compat import properties
|
||||
from ..credentials import SimpleCredential
|
||||
from ..errors import InitError, KeyringLocked, PasswordDeleteError, PasswordSetError
|
||||
|
||||
try:
|
||||
import dbus
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
except ImportError:
|
||||
pass
|
||||
except AttributeError:
|
||||
# See https://github.com/jaraco/keyring/issues/296
|
||||
pass
|
||||
|
||||
|
||||
def _id_from_argv():
|
||||
"""
|
||||
Safely infer an app id from sys.argv.
|
||||
"""
|
||||
allowed = AttributeError, IndexError, TypeError
|
||||
with contextlib.suppress(allowed):
|
||||
return sys.argv[0]
|
||||
|
||||
|
||||
class DBusKeyring(KeyringBackend):
|
||||
"""
|
||||
KDE KWallet 5 via D-Bus
|
||||
"""
|
||||
|
||||
appid = _id_from_argv() or 'Python keyring library'
|
||||
wallet = None
|
||||
bus_name = 'org.kde.kwalletd5'
|
||||
object_path = '/modules/kwalletd5'
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls) -> float:
|
||||
if 'dbus' not in globals():
|
||||
raise RuntimeError('python-dbus not installed')
|
||||
try:
|
||||
bus = dbus.SessionBus(mainloop=DBusGMainLoop())
|
||||
except dbus.DBusException as exc:
|
||||
raise RuntimeError(exc.get_dbus_message()) from exc
|
||||
if not (
|
||||
bus.name_has_owner(cls.bus_name)
|
||||
or cls.bus_name in bus.list_activatable_names()
|
||||
):
|
||||
raise RuntimeError(
|
||||
"The KWallet daemon is neither running nor activatable through D-Bus"
|
||||
)
|
||||
if "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "").split(":"):
|
||||
return 5.1
|
||||
return 4.9
|
||||
|
||||
def __init__(self, *arg, **kw):
|
||||
super().__init__(*arg, **kw)
|
||||
self.handle = -1
|
||||
|
||||
def _migrate(self, service):
|
||||
old_folder = 'Python'
|
||||
entry_list = []
|
||||
if self.iface.hasFolder(self.handle, old_folder, self.appid):
|
||||
entry_list = self.iface.readPasswordList(
|
||||
self.handle, old_folder, '*@*', self.appid
|
||||
)
|
||||
|
||||
for entry in entry_list.items():
|
||||
key = entry[0]
|
||||
password = entry[1]
|
||||
|
||||
username, service = key.rsplit('@', 1)
|
||||
ret = self.iface.writePassword(
|
||||
self.handle, service, username, password, self.appid
|
||||
)
|
||||
if ret == 0:
|
||||
self.iface.removeEntry(self.handle, old_folder, key, self.appid)
|
||||
|
||||
entry_list = self.iface.readPasswordList(
|
||||
self.handle, old_folder, '*', self.appid
|
||||
)
|
||||
if not entry_list:
|
||||
self.iface.removeFolder(self.handle, old_folder, self.appid)
|
||||
|
||||
def connected(self, service):
|
||||
if self.handle >= 0:
|
||||
if self.iface.isOpen(self.handle):
|
||||
return True
|
||||
|
||||
bus = dbus.SessionBus(mainloop=DBusGMainLoop())
|
||||
wId = 0
|
||||
try:
|
||||
remote_obj = bus.get_object(self.bus_name, self.object_path)
|
||||
self.iface = dbus.Interface(remote_obj, 'org.kde.KWallet')
|
||||
self.handle = self.iface.open(self.iface.networkWallet(), wId, self.appid)
|
||||
except dbus.DBusException as e:
|
||||
raise InitError(f'Failed to open keyring: {e}.') from e
|
||||
|
||||
if self.handle < 0:
|
||||
return False
|
||||
self._migrate(service)
|
||||
return True
|
||||
|
||||
def get_password(self, service, username):
|
||||
"""Get password of the username for the service"""
|
||||
if not self.connected(service):
|
||||
# the user pressed "cancel" when prompted to unlock their keyring.
|
||||
raise KeyringLocked("Failed to unlock the keyring!")
|
||||
if not self.iface.hasEntry(self.handle, service, username, self.appid):
|
||||
return None
|
||||
password = self.iface.readPassword(self.handle, service, username, self.appid)
|
||||
return str(password)
|
||||
|
||||
def get_credential(self, service, username):
|
||||
"""Gets the first username and password for a service.
|
||||
Returns a Credential instance
|
||||
|
||||
The username can be omitted, but if there is one, it will forward to
|
||||
get_password.
|
||||
Otherwise, it will return the first username and password combo that it finds.
|
||||
"""
|
||||
if username is not None:
|
||||
return super().get_credential(service, username)
|
||||
|
||||
if not self.connected(service):
|
||||
# the user pressed "cancel" when prompted to unlock their keyring.
|
||||
raise KeyringLocked("Failed to unlock the keyring!")
|
||||
|
||||
for username in self.iface.entryList(self.handle, service, self.appid):
|
||||
password = self.iface.readPassword(
|
||||
self.handle, service, username, self.appid
|
||||
)
|
||||
return SimpleCredential(str(username), str(password))
|
||||
|
||||
def set_password(self, service, username, password):
|
||||
"""Set password for the username of the service"""
|
||||
if not self.connected(service):
|
||||
# the user pressed "cancel" when prompted to unlock their keyring.
|
||||
raise PasswordSetError("Cancelled by user")
|
||||
self.iface.writePassword(self.handle, service, username, password, self.appid)
|
||||
|
||||
def delete_password(self, service, username):
|
||||
"""Delete the password for the username of the service."""
|
||||
if not self.connected(service):
|
||||
# the user pressed "cancel" when prompted to unlock their keyring.
|
||||
raise PasswordDeleteError("Cancelled by user")
|
||||
if not self.iface.hasEntry(self.handle, service, username, self.appid):
|
||||
raise PasswordDeleteError("Password not found")
|
||||
self.iface.removeEntry(self.handle, service, username, self.appid)
|
||||
|
||||
|
||||
class DBusKeyringKWallet4(DBusKeyring):
|
||||
"""
|
||||
KDE KWallet 4 via D-Bus
|
||||
"""
|
||||
|
||||
bus_name = 'org.kde.kwalletd'
|
||||
object_path = '/modules/kwalletd'
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls):
|
||||
return super().priority - 1
|
||||
@@ -0,0 +1,155 @@
|
||||
import logging
|
||||
|
||||
from .. import backend
|
||||
from ..backend import KeyringBackend
|
||||
from ..compat import properties
|
||||
from ..credentials import SimpleCredential
|
||||
from ..errors import (
|
||||
KeyringLocked,
|
||||
PasswordDeleteError,
|
||||
PasswordSetError,
|
||||
)
|
||||
|
||||
available = False
|
||||
try:
|
||||
import gi
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
gi.require_version('Secret', '1')
|
||||
from gi.repository import Secret
|
||||
|
||||
available = True
|
||||
except (AttributeError, ImportError, ValueError):
|
||||
pass
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Keyring(backend.SchemeSelectable, KeyringBackend):
|
||||
"""libsecret Keyring"""
|
||||
|
||||
appid = 'Python keyring library'
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
return Secret.Schema.new(
|
||||
"org.freedesktop.Secret.Generic",
|
||||
Secret.SchemaFlags.NONE,
|
||||
self._query(
|
||||
Secret.SchemaAttributeType.STRING,
|
||||
Secret.SchemaAttributeType.STRING,
|
||||
application=Secret.SchemaAttributeType.STRING,
|
||||
),
|
||||
)
|
||||
|
||||
@properties.NonDataProperty
|
||||
def collection(self):
|
||||
return Secret.COLLECTION_DEFAULT
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls) -> float:
|
||||
if not available:
|
||||
raise RuntimeError("libsecret required")
|
||||
|
||||
# Make sure there is actually a secret service running
|
||||
try:
|
||||
Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION, None)
|
||||
except GLib.Error as error:
|
||||
raise RuntimeError("Can't open a session to the secret service") from error
|
||||
|
||||
return 4.8
|
||||
|
||||
def get_password(self, service, username):
|
||||
"""Get password of the username for the service"""
|
||||
attributes = self._query(service, username, application=self.appid)
|
||||
try:
|
||||
items = Secret.password_search_sync(
|
||||
self.schema, attributes, Secret.SearchFlags.UNLOCK, None
|
||||
)
|
||||
except GLib.Error as error:
|
||||
quark = GLib.quark_try_string('g-io-error-quark')
|
||||
if error.matches(quark, Gio.IOErrorEnum.FAILED):
|
||||
raise KeyringLocked('Failed to unlock the item!') from error
|
||||
raise
|
||||
for item in items:
|
||||
try:
|
||||
return item.retrieve_secret_sync().get_text()
|
||||
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):
|
||||
"""Set password for the username of the service"""
|
||||
attributes = self._query(service, username, application=self.appid)
|
||||
label = f"Password for '{username}' on '{service}'"
|
||||
try:
|
||||
stored = Secret.password_store_sync(
|
||||
self.schema, attributes, self.collection, label, password, 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 collection!") from error
|
||||
quark = GLib.quark_try_string('g-io-error-quark')
|
||||
if error.matches(quark, Gio.IOErrorEnum.FAILED):
|
||||
raise KeyringLocked("Failed to unlock the collection!") from error
|
||||
raise
|
||||
if not stored:
|
||||
raise PasswordSetError("Failed to store password!")
|
||||
|
||||
def delete_password(self, service, username):
|
||||
"""Delete the stored password (only the first one)"""
|
||||
attributes = self._query(service, username, application=self.appid)
|
||||
try:
|
||||
items = Secret.password_search_sync(
|
||||
self.schema, attributes, Secret.SearchFlags.UNLOCK, None
|
||||
)
|
||||
except GLib.Error as error:
|
||||
quark = GLib.quark_try_string('g-io-error-quark')
|
||||
if error.matches(quark, Gio.IOErrorEnum.FAILED):
|
||||
raise KeyringLocked('Failed to unlock the item!') from error
|
||||
raise
|
||||
for item in items:
|
||||
try:
|
||||
removed = 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):
|
||||
"""Get the first username and password for a service.
|
||||
Return a Credential instance
|
||||
|
||||
The username can be omitted, but if there is one, it will use get_password
|
||||
and return a SimpleCredential containing the username and password
|
||||
Otherwise, it will return the first username and password combo that it finds.
|
||||
"""
|
||||
query = self._query(service, username)
|
||||
try:
|
||||
items = Secret.password_search_sync(
|
||||
self.schema, query, Secret.SearchFlags.UNLOCK, None
|
||||
)
|
||||
except GLib.Error as error:
|
||||
quark = GLib.quark_try_string('g-io-error-quark')
|
||||
if error.matches(quark, Gio.IOErrorEnum.FAILED):
|
||||
raise KeyringLocked('Failed to unlock the item!') from error
|
||||
raise
|
||||
for item in items:
|
||||
username = item.get_attributes().get("username")
|
||||
try:
|
||||
return SimpleCredential(
|
||||
username, item.retrieve_secret_sync().get_text()
|
||||
)
|
||||
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
|
||||
@@ -0,0 +1,85 @@
|
||||
import functools
|
||||
import os
|
||||
import platform
|
||||
import warnings
|
||||
|
||||
from ...backend import KeyringBackend
|
||||
from ...compat import properties
|
||||
from ...errors import KeyringError, KeyringLocked, PasswordDeleteError, PasswordSetError
|
||||
|
||||
try:
|
||||
from . import api
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def warn_keychain(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if self.keychain:
|
||||
warnings.warn("Specified keychain is ignored. See #623", stacklevel=2)
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Keyring(KeyringBackend):
|
||||
"""macOS Keychain"""
|
||||
|
||||
keychain = os.environ.get('KEYCHAIN_PATH')
|
||||
"Path to keychain file, overriding default"
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls):
|
||||
"""
|
||||
Preferred for all macOS environments.
|
||||
"""
|
||||
if platform.system() != 'Darwin':
|
||||
raise RuntimeError("macOS required")
|
||||
if 'api' not in globals():
|
||||
raise RuntimeError("Security API unavailable")
|
||||
return 5
|
||||
|
||||
@warn_keychain
|
||||
def set_password(self, service, username, password):
|
||||
if username is None:
|
||||
username = ''
|
||||
|
||||
try:
|
||||
api.set_generic_password(self.keychain, service, username, password)
|
||||
except api.KeychainDenied as e:
|
||||
raise KeyringLocked(f"Can't store password on keychain: {e}") from e
|
||||
except api.Error as e:
|
||||
raise PasswordSetError(f"Can't store password on keychain: {e}") from e
|
||||
|
||||
@warn_keychain
|
||||
def get_password(self, service, username):
|
||||
if username is None:
|
||||
username = ''
|
||||
|
||||
try:
|
||||
return api.find_generic_password(self.keychain, service, username)
|
||||
except api.NotFound:
|
||||
pass
|
||||
except api.KeychainDenied as e:
|
||||
raise KeyringLocked(f"Can't get password from keychain: {e}") from e
|
||||
except api.Error as e:
|
||||
raise KeyringError(f"Can't get password from keychain: {e}") from e
|
||||
|
||||
@warn_keychain
|
||||
def delete_password(self, service, username):
|
||||
if username is None:
|
||||
username = ''
|
||||
|
||||
try:
|
||||
return api.delete_generic_password(self.keychain, service, username)
|
||||
except api.Error as e:
|
||||
raise PasswordDeleteError(f"Can't delete password in keychain: {e}") from e
|
||||
|
||||
def with_keychain(self, keychain):
|
||||
warnings.warn(
|
||||
"macOS.Keyring.with_keychain is deprecated. Use with_properties instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.with_properties(keychain=keychain)
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,184 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import ctypes
|
||||
import functools
|
||||
from ctypes import (
|
||||
byref,
|
||||
c_int32,
|
||||
c_uint32,
|
||||
c_void_p,
|
||||
)
|
||||
from ctypes.util import find_library
|
||||
|
||||
OS_status = c_int32
|
||||
|
||||
|
||||
class error:
|
||||
item_not_found = -25300
|
||||
keychain_denied = -128
|
||||
sec_auth_failed = -25293
|
||||
plist_missing = -67030
|
||||
sec_interaction_not_allowed = -25308
|
||||
|
||||
|
||||
_sec = ctypes.CDLL(find_library('Security'))
|
||||
_core = ctypes.CDLL(find_library('CoreServices'))
|
||||
_found = ctypes.CDLL(find_library('Foundation'))
|
||||
|
||||
CFDictionaryCreate = _found.CFDictionaryCreate
|
||||
CFDictionaryCreate.restype = c_void_p
|
||||
CFDictionaryCreate.argtypes = (
|
||||
c_void_p,
|
||||
c_void_p,
|
||||
c_void_p,
|
||||
c_int32,
|
||||
c_void_p,
|
||||
c_void_p,
|
||||
)
|
||||
|
||||
CFStringCreateWithCString = _found.CFStringCreateWithCString
|
||||
CFStringCreateWithCString.restype = c_void_p
|
||||
CFStringCreateWithCString.argtypes = [c_void_p, c_void_p, c_uint32]
|
||||
|
||||
CFNumberCreate = _found.CFNumberCreate
|
||||
CFNumberCreate.restype = c_void_p
|
||||
CFNumberCreate.argtypes = [c_void_p, c_uint32, ctypes.c_void_p]
|
||||
|
||||
SecItemAdd = _sec.SecItemAdd
|
||||
SecItemAdd.restype = OS_status
|
||||
SecItemAdd.argtypes = (c_void_p, c_void_p)
|
||||
|
||||
SecItemCopyMatching = _sec.SecItemCopyMatching
|
||||
SecItemCopyMatching.restype = OS_status
|
||||
SecItemCopyMatching.argtypes = (c_void_p, c_void_p)
|
||||
|
||||
SecItemDelete = _sec.SecItemDelete
|
||||
SecItemDelete.restype = OS_status
|
||||
SecItemDelete.argtypes = (c_void_p,)
|
||||
|
||||
CFDataGetBytePtr = _found.CFDataGetBytePtr
|
||||
CFDataGetBytePtr.restype = c_void_p
|
||||
CFDataGetBytePtr.argtypes = (c_void_p,)
|
||||
|
||||
CFDataGetLength = _found.CFDataGetLength
|
||||
CFDataGetLength.restype = c_int32
|
||||
CFDataGetLength.argtypes = (c_void_p,)
|
||||
|
||||
|
||||
def k_(s):
|
||||
return c_void_p.in_dll(_sec, s)
|
||||
|
||||
|
||||
@functools.singledispatch
|
||||
def create_cf(ob):
|
||||
return ob
|
||||
|
||||
|
||||
# explicit bool and int required for Python 3.10 compatibility
|
||||
@create_cf.register(bool)
|
||||
@create_cf.register(int)
|
||||
def _(val: bool | int):
|
||||
if val.bit_length() > 31:
|
||||
raise OverflowError(val)
|
||||
int32 = 0x9
|
||||
return CFNumberCreate(None, int32, ctypes.byref(c_int32(val)))
|
||||
|
||||
|
||||
@create_cf.register
|
||||
def _(s: str):
|
||||
kCFStringEncodingUTF8 = 0x08000100
|
||||
return CFStringCreateWithCString(None, s.encode('utf8'), kCFStringEncodingUTF8)
|
||||
|
||||
|
||||
def create_query(**kwargs):
|
||||
return CFDictionaryCreate(
|
||||
None,
|
||||
(c_void_p * len(kwargs))(*map(k_, kwargs.keys())),
|
||||
(c_void_p * len(kwargs))(*map(create_cf, kwargs.values())),
|
||||
len(kwargs),
|
||||
_found.kCFTypeDictionaryKeyCallBacks,
|
||||
_found.kCFTypeDictionaryValueCallBacks,
|
||||
)
|
||||
|
||||
|
||||
def cfstr_to_str(data):
|
||||
return ctypes.string_at(CFDataGetBytePtr(data), CFDataGetLength(data)).decode(
|
||||
'utf-8'
|
||||
)
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
@classmethod
|
||||
def raise_for_status(cls, status):
|
||||
if status == 0:
|
||||
return
|
||||
if status == error.item_not_found:
|
||||
raise NotFound(status, "Item not found")
|
||||
if status == error.keychain_denied:
|
||||
raise KeychainDenied(status, "Keychain Access Denied")
|
||||
if status == error.sec_auth_failed or status == error.plist_missing:
|
||||
raise SecAuthFailure(
|
||||
status,
|
||||
"Security Auth Failure: make sure "
|
||||
"executable is signed with codesign util",
|
||||
)
|
||||
raise cls(status, "Unknown Error")
|
||||
|
||||
|
||||
class NotFound(Error):
|
||||
pass
|
||||
|
||||
|
||||
class KeychainDenied(Error):
|
||||
pass
|
||||
|
||||
|
||||
class SecAuthFailure(Error):
|
||||
pass
|
||||
|
||||
|
||||
def find_generic_password(kc_name, service, username, not_found_ok=False):
|
||||
q = create_query(
|
||||
kSecClass=k_('kSecClassGenericPassword'),
|
||||
kSecMatchLimit=k_('kSecMatchLimitOne'),
|
||||
kSecAttrService=service,
|
||||
kSecAttrAccount=username,
|
||||
kSecReturnData=True,
|
||||
)
|
||||
|
||||
data = c_void_p()
|
||||
status = SecItemCopyMatching(q, byref(data))
|
||||
|
||||
if status == error.item_not_found and not_found_ok:
|
||||
return
|
||||
|
||||
Error.raise_for_status(status)
|
||||
|
||||
return cfstr_to_str(data)
|
||||
|
||||
|
||||
def set_generic_password(name, service, username, password):
|
||||
with contextlib.suppress(NotFound):
|
||||
delete_generic_password(name, service, username)
|
||||
|
||||
q = create_query(
|
||||
kSecClass=k_('kSecClassGenericPassword'),
|
||||
kSecAttrService=service,
|
||||
kSecAttrAccount=username,
|
||||
kSecValueData=password,
|
||||
)
|
||||
|
||||
status = SecItemAdd(q, None)
|
||||
Error.raise_for_status(status)
|
||||
|
||||
|
||||
def delete_generic_password(name, service, username):
|
||||
q = create_query(
|
||||
kSecClass=k_('kSecClassGenericPassword'),
|
||||
kSecAttrService=service,
|
||||
kSecAttrAccount=username,
|
||||
)
|
||||
|
||||
status = SecItemDelete(q)
|
||||
Error.raise_for_status(status)
|
||||
@@ -0,0 +1,20 @@
|
||||
from ..backend import KeyringBackend
|
||||
from ..compat import properties
|
||||
|
||||
|
||||
class Keyring(KeyringBackend):
|
||||
"""
|
||||
Keyring that return None on every operation.
|
||||
|
||||
>>> kr = Keyring()
|
||||
>>> kr.get_password('svc', 'user')
|
||||
"""
|
||||
|
||||
@properties.classproperty
|
||||
def priority(cls) -> float:
|
||||
return -1
|
||||
|
||||
def get_password(self, service, username, password=None):
|
||||
pass
|
||||
|
||||
set_password = delete_password = get_password
|
||||
Reference in New Issue
Block a user