initial commit

This commit is contained in:
Davidson Gomes
2024-10-30 11:19:09 -03:00
commit 8654a31a4d
3744 changed files with 585542 additions and 0 deletions

View File

@@ -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'))

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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