mirror of
https://github.com/EvolutionAPI/evolution-client-python.git
synced 2025-12-26 06:57:44 -06:00
initial commit
This commit is contained in:
93
env/lib/python3.10/site-packages/secretstorage/__init__.py
vendored
Normal file
93
env/lib/python3.10/site-packages/secretstorage/__init__.py
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
# SecretStorage module for Python
|
||||
# Access passwords using the SecretService DBus API
|
||||
# Author: Dmitry Shachnev, 2013-2020
|
||||
# License: 3-clause BSD, see LICENSE file
|
||||
|
||||
"""This file provides quick access to all SecretStorage API. Please
|
||||
refer to documentation of individual modules for API details.
|
||||
"""
|
||||
|
||||
from jeepney.bus_messages import message_bus
|
||||
from jeepney.io.blocking import DBusConnection, Proxy, open_dbus_connection
|
||||
|
||||
from secretstorage.collection import Collection, create_collection, \
|
||||
get_all_collections, get_default_collection, get_any_collection, \
|
||||
get_collection_by_alias, search_items
|
||||
from secretstorage.item import Item
|
||||
from secretstorage.exceptions import SecretStorageException, \
|
||||
SecretServiceNotAvailableException, LockedException, \
|
||||
ItemNotFoundException, PromptDismissedException
|
||||
from secretstorage.util import add_match_rules
|
||||
|
||||
__version_tuple__ = (3, 3, 3)
|
||||
__version__ = '.'.join(map(str, __version_tuple__))
|
||||
|
||||
__all__ = [
|
||||
'Collection',
|
||||
'Item',
|
||||
'ItemNotFoundException',
|
||||
'LockedException',
|
||||
'PromptDismissedException',
|
||||
'SecretServiceNotAvailableException',
|
||||
'SecretStorageException',
|
||||
'check_service_availability',
|
||||
'create_collection',
|
||||
'dbus_init',
|
||||
'get_all_collections',
|
||||
'get_any_collection',
|
||||
'get_collection_by_alias',
|
||||
'get_default_collection',
|
||||
'search_items',
|
||||
]
|
||||
|
||||
|
||||
def dbus_init() -> DBusConnection:
|
||||
"""Returns a new connection to the session bus, instance of
|
||||
jeepney's :class:`DBusConnection` class. This connection can
|
||||
then be passed to various SecretStorage functions, such as
|
||||
:func:`~secretstorage.collection.get_default_collection`.
|
||||
|
||||
.. warning::
|
||||
The D-Bus socket will not be closed automatically. You can
|
||||
close it manually using the :meth:`DBusConnection.close` method,
|
||||
or you can use the :class:`contextlib.closing` context manager:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import closing
|
||||
with closing(dbus_init()) as conn:
|
||||
collection = secretstorage.get_default_collection(conn)
|
||||
items = collection.search_items({'application': 'myapp'})
|
||||
|
||||
However, you will not be able to call any methods on the objects
|
||||
created within the context after you leave it.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
Before the port to Jeepney, this function returned an
|
||||
instance of :class:`dbus.SessionBus` class.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
This function no longer accepts any arguments.
|
||||
"""
|
||||
try:
|
||||
connection = open_dbus_connection()
|
||||
add_match_rules(connection)
|
||||
return connection
|
||||
except KeyError as ex:
|
||||
# os.environ['DBUS_SESSION_BUS_ADDRESS'] may raise it
|
||||
reason = "Environment variable {} is unset".format(ex.args[0])
|
||||
raise SecretServiceNotAvailableException(reason) from ex
|
||||
except (ConnectionError, ValueError) as ex:
|
||||
raise SecretServiceNotAvailableException(str(ex)) from ex
|
||||
|
||||
|
||||
def check_service_availability(connection: DBusConnection) -> bool:
|
||||
"""Returns True if the Secret Service daemon is either running or
|
||||
available for activation via D-Bus, False otherwise.
|
||||
|
||||
.. versionadded:: 3.2
|
||||
"""
|
||||
from secretstorage.util import BUS_NAME
|
||||
proxy = Proxy(message_bus, connection)
|
||||
return (proxy.NameHasOwner(BUS_NAME)[0] == 1
|
||||
or BUS_NAME in proxy.ListActivatableNames()[0])
|
||||
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/collection.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/collection.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/defines.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/defines.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/dhcrypto.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/dhcrypto.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/exceptions.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/exceptions.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/item.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/item.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/util.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/secretstorage/__pycache__/util.cpython-310.pyc
vendored
Normal file
Binary file not shown.
224
env/lib/python3.10/site-packages/secretstorage/collection.py
vendored
Normal file
224
env/lib/python3.10/site-packages/secretstorage/collection.py
vendored
Normal file
@@ -0,0 +1,224 @@
|
||||
# SecretStorage module for Python
|
||||
# Access passwords using the SecretService DBus API
|
||||
# Author: Dmitry Shachnev, 2013-2022
|
||||
# License: 3-clause BSD, see LICENSE file
|
||||
|
||||
"""Collection is a place where secret items are stored. Normally, only
|
||||
the default collection should be used, but this module allows to use any
|
||||
registered collection. Use :func:`get_default_collection` to get the
|
||||
default collection (and create it, if necessary).
|
||||
|
||||
Collections are usually automatically unlocked when user logs in, but
|
||||
collections can also be locked and unlocked using
|
||||
:meth:`Collection.lock` and :meth:`Collection.unlock` methods (unlocking
|
||||
requires showing the unlocking prompt to user and can be synchronous or
|
||||
asynchronous). Creating new items and editing existing ones is possible
|
||||
only in unlocked collection."""
|
||||
|
||||
from typing import Dict, Iterator, Optional
|
||||
from jeepney.io.blocking import DBusConnection
|
||||
from secretstorage.defines import SS_PREFIX, SS_PATH
|
||||
from secretstorage.dhcrypto import Session
|
||||
from secretstorage.exceptions import LockedException, ItemNotFoundException, \
|
||||
PromptDismissedException
|
||||
from secretstorage.item import Item
|
||||
from secretstorage.util import DBusAddressWrapper, exec_prompt, \
|
||||
format_secret, open_session, unlock_objects
|
||||
|
||||
COLLECTION_IFACE = SS_PREFIX + 'Collection'
|
||||
SERVICE_IFACE = SS_PREFIX + 'Service'
|
||||
DEFAULT_COLLECTION = '/org/freedesktop/secrets/aliases/default'
|
||||
SESSION_COLLECTION = '/org/freedesktop/secrets/collection/session'
|
||||
|
||||
|
||||
class Collection:
|
||||
"""Represents a collection."""
|
||||
|
||||
def __init__(self, connection: DBusConnection,
|
||||
collection_path: str = DEFAULT_COLLECTION,
|
||||
session: Optional[Session] = None) -> None:
|
||||
self.connection = connection
|
||||
self.session = session
|
||||
self.collection_path = collection_path
|
||||
self._collection = DBusAddressWrapper(
|
||||
collection_path, COLLECTION_IFACE, connection)
|
||||
self._collection.get_property('Label')
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
"""Returns :const:`True` if item is locked, otherwise
|
||||
:const:`False`."""
|
||||
return bool(self._collection.get_property('Locked'))
|
||||
|
||||
def ensure_not_locked(self) -> None:
|
||||
"""If collection is locked, raises
|
||||
:exc:`~secretstorage.exceptions.LockedException`."""
|
||||
if self.is_locked():
|
||||
raise LockedException('Collection is locked!')
|
||||
|
||||
def unlock(self) -> bool:
|
||||
"""Requests unlocking the collection.
|
||||
|
||||
Returns a boolean representing whether the prompt has been
|
||||
dismissed; that means :const:`False` on successful unlocking
|
||||
and :const:`True` if it has been dismissed.
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
No longer accepts the ``callback`` argument.
|
||||
"""
|
||||
return unlock_objects(self.connection, [self.collection_path])
|
||||
|
||||
def lock(self) -> None:
|
||||
"""Locks the collection."""
|
||||
service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, self.connection)
|
||||
service.call('Lock', 'ao', [self.collection_path])
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Deletes the collection and all items inside it."""
|
||||
self.ensure_not_locked()
|
||||
prompt, = self._collection.call('Delete', '')
|
||||
if prompt != "/":
|
||||
dismissed, _result = exec_prompt(self.connection, prompt)
|
||||
if dismissed:
|
||||
raise PromptDismissedException('Prompt dismissed.')
|
||||
|
||||
def get_all_items(self) -> Iterator[Item]:
|
||||
"""Returns a generator of all items in the collection."""
|
||||
for item_path in self._collection.get_property('Items'):
|
||||
yield Item(self.connection, item_path, self.session)
|
||||
|
||||
def search_items(self, attributes: Dict[str, str]) -> Iterator[Item]:
|
||||
"""Returns a generator of items with the given attributes.
|
||||
`attributes` should be a dictionary."""
|
||||
result, = self._collection.call('SearchItems', 'a{ss}', attributes)
|
||||
for item_path in result:
|
||||
yield Item(self.connection, item_path, self.session)
|
||||
|
||||
def get_label(self) -> str:
|
||||
"""Returns the collection label."""
|
||||
label = self._collection.get_property('Label')
|
||||
assert isinstance(label, str)
|
||||
return label
|
||||
|
||||
def set_label(self, label: str) -> None:
|
||||
"""Sets collection label to `label`."""
|
||||
self.ensure_not_locked()
|
||||
self._collection.set_property('Label', 's', label)
|
||||
|
||||
def create_item(self, label: str, attributes: Dict[str, str],
|
||||
secret: bytes, replace: bool = False,
|
||||
content_type: str = 'text/plain') -> Item:
|
||||
"""Creates a new :class:`~secretstorage.item.Item` with given
|
||||
`label` (unicode string), `attributes` (dictionary) and `secret`
|
||||
(bytestring). If `replace` is :const:`True`, replaces the existing
|
||||
item with the same attributes. If `content_type` is given, also
|
||||
sets the content type of the secret (``text/plain`` by default).
|
||||
Returns the created item."""
|
||||
self.ensure_not_locked()
|
||||
if not self.session:
|
||||
self.session = open_session(self.connection)
|
||||
_secret = format_secret(self.session, secret, content_type)
|
||||
properties = {
|
||||
SS_PREFIX + 'Item.Label': ('s', label),
|
||||
SS_PREFIX + 'Item.Attributes': ('a{ss}', attributes),
|
||||
}
|
||||
item_path, prompt = self._collection.call(
|
||||
'CreateItem',
|
||||
'a{sv}(oayays)b',
|
||||
properties,
|
||||
_secret,
|
||||
replace
|
||||
)
|
||||
if len(item_path) > 1:
|
||||
return Item(self.connection, item_path, self.session)
|
||||
dismissed, result = exec_prompt(self.connection, prompt)
|
||||
if dismissed:
|
||||
raise PromptDismissedException('Prompt dismissed.')
|
||||
signature, item_path = result
|
||||
assert signature == 'o'
|
||||
return Item(self.connection, item_path, self.session)
|
||||
|
||||
|
||||
def create_collection(connection: DBusConnection, label: str, alias: str = '',
|
||||
session: Optional[Session] = None) -> Collection:
|
||||
"""Creates a new :class:`Collection` with the given `label` and `alias`
|
||||
and returns it. This action requires prompting.
|
||||
|
||||
:raises: :exc:`~secretstorage.exceptions.PromptDismissedException`
|
||||
if the prompt is dismissed.
|
||||
"""
|
||||
if not session:
|
||||
session = open_session(connection)
|
||||
properties = {SS_PREFIX + 'Collection.Label': ('s', label)}
|
||||
service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection)
|
||||
collection_path, prompt = service.call('CreateCollection', 'a{sv}s',
|
||||
properties, alias)
|
||||
if len(collection_path) > 1:
|
||||
return Collection(connection, collection_path, session=session)
|
||||
dismissed, result = exec_prompt(connection, prompt)
|
||||
if dismissed:
|
||||
raise PromptDismissedException('Prompt dismissed.')
|
||||
signature, collection_path = result
|
||||
assert signature == 'o'
|
||||
return Collection(connection, collection_path, session=session)
|
||||
|
||||
|
||||
def get_all_collections(connection: DBusConnection) -> Iterator[Collection]:
|
||||
"""Returns a generator of all available collections."""
|
||||
service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection)
|
||||
for collection_path in service.get_property('Collections'):
|
||||
yield Collection(connection, collection_path)
|
||||
|
||||
|
||||
def get_default_collection(connection: DBusConnection,
|
||||
session: Optional[Session] = None) -> Collection:
|
||||
"""Returns the default collection. If it doesn't exist,
|
||||
creates it."""
|
||||
try:
|
||||
return Collection(connection)
|
||||
except ItemNotFoundException:
|
||||
return create_collection(connection, 'Default', 'default', session)
|
||||
|
||||
|
||||
def get_any_collection(connection: DBusConnection) -> Collection:
|
||||
"""Returns any collection, in the following order of preference:
|
||||
|
||||
- The default collection;
|
||||
- The "session" collection (usually temporary);
|
||||
- The first collection in the collections list."""
|
||||
try:
|
||||
return Collection(connection)
|
||||
except ItemNotFoundException:
|
||||
pass
|
||||
try:
|
||||
# GNOME Keyring provides session collection where items
|
||||
# are stored in process memory.
|
||||
return Collection(connection, SESSION_COLLECTION)
|
||||
except ItemNotFoundException:
|
||||
pass
|
||||
collections = list(get_all_collections(connection))
|
||||
if collections:
|
||||
return collections[0]
|
||||
else:
|
||||
raise ItemNotFoundException('No collections found.')
|
||||
|
||||
|
||||
def get_collection_by_alias(connection: DBusConnection,
|
||||
alias: str) -> Collection:
|
||||
"""Returns the collection with the given `alias`. If there is no
|
||||
such collection, raises
|
||||
:exc:`~secretstorage.exceptions.ItemNotFoundException`."""
|
||||
service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection)
|
||||
collection_path, = service.call('ReadAlias', 's', alias)
|
||||
if len(collection_path) <= 1:
|
||||
raise ItemNotFoundException('No collection with such alias.')
|
||||
return Collection(connection, collection_path)
|
||||
|
||||
|
||||
def search_items(connection: DBusConnection,
|
||||
attributes: Dict[str, str]) -> Iterator[Item]:
|
||||
"""Returns a generator of items in all collections with the given
|
||||
attributes. `attributes` should be a dictionary."""
|
||||
service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection)
|
||||
locked, unlocked = service.call('SearchItems', 'a{ss}', attributes)
|
||||
for item_path in locked + unlocked:
|
||||
yield Item(connection, item_path)
|
||||
20
env/lib/python3.10/site-packages/secretstorage/defines.py
vendored
Normal file
20
env/lib/python3.10/site-packages/secretstorage/defines.py
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# SecretStorage module for Python
|
||||
# Access passwords using the SecretService DBus API
|
||||
# Author: Dmitry Shachnev, 2013-2016
|
||||
# License: 3-clause BSD, see LICENSE file
|
||||
|
||||
# This file contains some common defines.
|
||||
|
||||
SS_PREFIX = 'org.freedesktop.Secret.'
|
||||
SS_PATH = '/org/freedesktop/secrets'
|
||||
|
||||
DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
|
||||
DBUS_ACCESS_DENIED = 'org.freedesktop.DBus.Error.AccessDenied'
|
||||
DBUS_SERVICE_UNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown'
|
||||
DBUS_EXEC_FAILED = 'org.freedesktop.DBus.Error.Spawn.ExecFailed'
|
||||
DBUS_NO_REPLY = 'org.freedesktop.DBus.Error.NoReply'
|
||||
DBUS_NOT_SUPPORTED = 'org.freedesktop.DBus.Error.NotSupported'
|
||||
DBUS_NO_SUCH_OBJECT = 'org.freedesktop.Secret.Error.NoSuchObject'
|
||||
|
||||
ALGORITHM_PLAIN = 'plain'
|
||||
ALGORITHM_DH = 'dh-ietf1024-sha256-aes128-cbc-pkcs7'
|
||||
59
env/lib/python3.10/site-packages/secretstorage/dhcrypto.py
vendored
Normal file
59
env/lib/python3.10/site-packages/secretstorage/dhcrypto.py
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
# SecretStorage module for Python
|
||||
# Access passwords using the SecretService DBus API
|
||||
# Author: Dmitry Shachnev, 2014-2018
|
||||
# License: 3-clause BSD, see LICENSE file
|
||||
|
||||
'''This module contains needed classes, functions and constants
|
||||
to implement dh-ietf1024-sha256-aes128-cbc-pkcs7 secret encryption
|
||||
algorithm.'''
|
||||
|
||||
import hmac
|
||||
import math
|
||||
import os
|
||||
|
||||
from hashlib import sha256
|
||||
from typing import Optional # Needed for mypy
|
||||
|
||||
# A standard 1024 bits (128 bytes) prime number for use in Diffie-Hellman exchange
|
||||
DH_PRIME_1024_BYTES = (
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68,
|
||||
0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, 0x29, 0x02, 0x4E, 0x08,
|
||||
0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A,
|
||||
0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B,
|
||||
0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51,
|
||||
0xC2, 0x45, 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9,
|
||||
0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, 0xEE, 0x38,
|
||||
0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6,
|
||||
0x49, 0x28, 0x66, 0x51, 0xEC, 0xE6, 0x53, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF
|
||||
)
|
||||
|
||||
|
||||
def int_to_bytes(number: int) -> bytes:
|
||||
return number.to_bytes(math.ceil(number.bit_length() / 8), 'big')
|
||||
|
||||
|
||||
DH_PRIME_1024 = int.from_bytes(DH_PRIME_1024_BYTES, 'big')
|
||||
|
||||
|
||||
class Session:
|
||||
def __init__(self) -> None:
|
||||
self.object_path = None # type: Optional[str]
|
||||
self.aes_key = None # type: Optional[bytes]
|
||||
self.encrypted = True
|
||||
# 128-bytes-long strong random number
|
||||
self.my_private_key = int.from_bytes(os.urandom(0x80), 'big')
|
||||
self.my_public_key = pow(2, self.my_private_key, DH_PRIME_1024)
|
||||
|
||||
def set_server_public_key(self, server_public_key: int) -> None:
|
||||
common_secret_int = pow(server_public_key, self.my_private_key,
|
||||
DH_PRIME_1024)
|
||||
common_secret = int_to_bytes(common_secret_int)
|
||||
# Prepend NULL bytes if needed
|
||||
common_secret = b'\x00' * (0x80 - len(common_secret)) + common_secret
|
||||
# HKDF with null salt, empty info and SHA-256 hash
|
||||
salt = b'\x00' * 0x20
|
||||
pseudo_random_key = hmac.new(salt, common_secret, sha256).digest()
|
||||
output_block = hmac.new(pseudo_random_key, b'\x01', sha256).digest()
|
||||
# Resulting AES key should be 128-bit
|
||||
self.aes_key = output_block[:0x10]
|
||||
50
env/lib/python3.10/site-packages/secretstorage/exceptions.py
vendored
Normal file
50
env/lib/python3.10/site-packages/secretstorage/exceptions.py
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
# SecretStorage module for Python
|
||||
# Access passwords using the SecretService DBus API
|
||||
# Author: Dmitry Shachnev, 2012-2018
|
||||
# License: 3-clause BSD, see LICENSE file
|
||||
|
||||
"""All secretstorage functions may raise various exceptions when
|
||||
something goes wrong. All exceptions derive from base
|
||||
:exc:`SecretStorageException` class."""
|
||||
|
||||
|
||||
class SecretStorageException(Exception):
|
||||
"""All exceptions derive from this class."""
|
||||
|
||||
|
||||
class SecretServiceNotAvailableException(SecretStorageException):
|
||||
"""Raised by :class:`~secretstorage.item.Item` or
|
||||
:class:`~secretstorage.collection.Collection` constructors, or by
|
||||
other functions in the :mod:`secretstorage.collection` module, when
|
||||
the Secret Service API is not available."""
|
||||
|
||||
|
||||
class LockedException(SecretStorageException):
|
||||
"""Raised when an action cannot be performed because the collection
|
||||
is locked. Use :meth:`~secretstorage.collection.Collection.is_locked`
|
||||
to check if the collection is locked, and
|
||||
:meth:`~secretstorage.collection.Collection.unlock` to unlock it.
|
||||
"""
|
||||
|
||||
|
||||
class ItemNotFoundException(SecretStorageException):
|
||||
"""Raised when an item does not exist or has been deleted. Example of
|
||||
handling:
|
||||
|
||||
>>> import secretstorage
|
||||
>>> connection = secretstorage.dbus_init()
|
||||
>>> item_path = '/not/existing/path'
|
||||
>>> try:
|
||||
... item = secretstorage.Item(connection, item_path)
|
||||
... except secretstorage.ItemNotFoundException:
|
||||
... print('Item not found!')
|
||||
...
|
||||
Item not found!
|
||||
"""
|
||||
|
||||
|
||||
class PromptDismissedException(ItemNotFoundException):
|
||||
"""Raised when a prompt was dismissed by the user.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
"""
|
||||
145
env/lib/python3.10/site-packages/secretstorage/item.py
vendored
Normal file
145
env/lib/python3.10/site-packages/secretstorage/item.py
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
# SecretStorage module for Python
|
||||
# Access passwords using the SecretService DBus API
|
||||
# Author: Dmitry Shachnev, 2013-2018
|
||||
# License: 3-clause BSD, see LICENSE file
|
||||
|
||||
"""SecretStorage item contains a *secret*, some *attributes* and a
|
||||
*label* visible to user. Editing all these properties and reading the
|
||||
secret is possible only when the :doc:`collection <collection>` storing
|
||||
the item is unlocked. The collection can be unlocked using collection's
|
||||
:meth:`~secretstorage.collection.Collection.unlock` method."""
|
||||
|
||||
from typing import Dict, Optional
|
||||
from jeepney.io.blocking import DBusConnection
|
||||
from secretstorage.defines import SS_PREFIX
|
||||
from secretstorage.dhcrypto import Session
|
||||
from secretstorage.exceptions import LockedException, PromptDismissedException
|
||||
from secretstorage.util import DBusAddressWrapper, \
|
||||
exec_prompt, open_session, format_secret, unlock_objects
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
ITEM_IFACE = SS_PREFIX + 'Item'
|
||||
|
||||
|
||||
class Item:
|
||||
"""Represents a secret item."""
|
||||
|
||||
def __init__(self, connection: DBusConnection,
|
||||
item_path: str, session: Optional[Session] = None) -> None:
|
||||
self.item_path = item_path
|
||||
self._item = DBusAddressWrapper(item_path, ITEM_IFACE, connection)
|
||||
self._item.get_property('Label')
|
||||
self.session = session
|
||||
self.connection = connection
|
||||
|
||||
def __eq__(self, other: "DBusConnection") -> bool:
|
||||
assert isinstance(other.item_path, str)
|
||||
return self.item_path == other.item_path
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
"""Returns :const:`True` if item is locked, otherwise
|
||||
:const:`False`."""
|
||||
return bool(self._item.get_property('Locked'))
|
||||
|
||||
def ensure_not_locked(self) -> None:
|
||||
"""If collection is locked, raises
|
||||
:exc:`~secretstorage.exceptions.LockedException`."""
|
||||
if self.is_locked():
|
||||
raise LockedException('Item is locked!')
|
||||
|
||||
def unlock(self) -> bool:
|
||||
"""Requests unlocking the item. Usually, this means that the
|
||||
whole collection containing this item will be unlocked.
|
||||
|
||||
Returns a boolean representing whether the prompt has been
|
||||
dismissed; that means :const:`False` on successful unlocking
|
||||
and :const:`True` if it has been dismissed.
|
||||
|
||||
.. versionadded:: 2.1.2
|
||||
|
||||
.. versionchanged:: 3.0
|
||||
No longer accepts the ``callback`` argument.
|
||||
"""
|
||||
return unlock_objects(self.connection, [self.item_path])
|
||||
|
||||
def get_attributes(self) -> Dict[str, str]:
|
||||
"""Returns item attributes (dictionary)."""
|
||||
attrs = self._item.get_property('Attributes')
|
||||
return dict(attrs)
|
||||
|
||||
def set_attributes(self, attributes: Dict[str, str]) -> None:
|
||||
"""Sets item attributes to `attributes` (dictionary)."""
|
||||
self._item.set_property('Attributes', 'a{ss}', attributes)
|
||||
|
||||
def get_label(self) -> str:
|
||||
"""Returns item label (unicode string)."""
|
||||
label = self._item.get_property('Label')
|
||||
assert isinstance(label, str)
|
||||
return label
|
||||
|
||||
def set_label(self, label: str) -> None:
|
||||
"""Sets item label to `label`."""
|
||||
self.ensure_not_locked()
|
||||
self._item.set_property('Label', 's', label)
|
||||
|
||||
def delete(self) -> None:
|
||||
"""Deletes the item."""
|
||||
self.ensure_not_locked()
|
||||
prompt, = self._item.call('Delete', '')
|
||||
if prompt != "/":
|
||||
dismissed, _result = exec_prompt(self.connection, prompt)
|
||||
if dismissed:
|
||||
raise PromptDismissedException('Prompt dismissed.')
|
||||
|
||||
def get_secret(self) -> bytes:
|
||||
"""Returns item secret (bytestring)."""
|
||||
self.ensure_not_locked()
|
||||
if not self.session:
|
||||
self.session = open_session(self.connection)
|
||||
secret, = self._item.call('GetSecret', 'o', self.session.object_path)
|
||||
if not self.session.encrypted:
|
||||
return bytes(secret[2])
|
||||
assert self.session.aes_key is not None
|
||||
aes = algorithms.AES(self.session.aes_key)
|
||||
aes_iv = bytes(secret[1])
|
||||
decryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).decryptor()
|
||||
encrypted_secret = secret[2]
|
||||
padded_secret = decryptor.update(bytes(encrypted_secret)) + decryptor.finalize()
|
||||
assert isinstance(padded_secret, bytes)
|
||||
return padded_secret[:-padded_secret[-1]]
|
||||
|
||||
def get_secret_content_type(self) -> str:
|
||||
"""Returns content type of item secret (string)."""
|
||||
self.ensure_not_locked()
|
||||
if not self.session:
|
||||
self.session = open_session(self.connection)
|
||||
secret, = self._item.call('GetSecret', 'o', self.session.object_path)
|
||||
return str(secret[3])
|
||||
|
||||
def set_secret(self, secret: bytes,
|
||||
content_type: str = 'text/plain') -> None:
|
||||
"""Sets item secret to `secret`. If `content_type` is given,
|
||||
also sets the content type of the secret (``text/plain`` by
|
||||
default)."""
|
||||
self.ensure_not_locked()
|
||||
if not self.session:
|
||||
self.session = open_session(self.connection)
|
||||
_secret = format_secret(self.session, secret, content_type)
|
||||
self._item.call('SetSecret', '(oayays)', _secret)
|
||||
|
||||
def get_created(self) -> int:
|
||||
"""Returns UNIX timestamp (integer) representing the time
|
||||
when the item was created.
|
||||
|
||||
.. versionadded:: 1.1"""
|
||||
created = self._item.get_property('Created')
|
||||
assert isinstance(created, int)
|
||||
return created
|
||||
|
||||
def get_modified(self) -> int:
|
||||
"""Returns UNIX timestamp (integer) representing the time
|
||||
when the item was last modified."""
|
||||
modified = self._item.get_property('Modified')
|
||||
assert isinstance(modified, int)
|
||||
return modified
|
||||
0
env/lib/python3.10/site-packages/secretstorage/py.typed
vendored
Normal file
0
env/lib/python3.10/site-packages/secretstorage/py.typed
vendored
Normal file
179
env/lib/python3.10/site-packages/secretstorage/util.py
vendored
Normal file
179
env/lib/python3.10/site-packages/secretstorage/util.py
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
# SecretStorage module for Python
|
||||
# Access passwords using the SecretService DBus API
|
||||
# Author: Dmitry Shachnev, 2013-2018
|
||||
# License: 3-clause BSD, see LICENSE file
|
||||
|
||||
"""This module provides some utility functions, but these shouldn't
|
||||
normally be used by external applications."""
|
||||
|
||||
import os
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
from jeepney import (
|
||||
DBusAddress, DBusErrorResponse, MatchRule, Message, MessageType,
|
||||
new_method_call, Properties,
|
||||
)
|
||||
from jeepney.io.blocking import DBusConnection
|
||||
from secretstorage.defines import DBUS_UNKNOWN_METHOD, DBUS_NO_SUCH_OBJECT, \
|
||||
DBUS_SERVICE_UNKNOWN, DBUS_NO_REPLY, DBUS_NOT_SUPPORTED, DBUS_EXEC_FAILED, \
|
||||
SS_PATH, SS_PREFIX, ALGORITHM_DH, ALGORITHM_PLAIN
|
||||
from secretstorage.dhcrypto import Session, int_to_bytes
|
||||
from secretstorage.exceptions import ItemNotFoundException, \
|
||||
SecretServiceNotAvailableException
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
BUS_NAME = 'org.freedesktop.secrets'
|
||||
SERVICE_IFACE = SS_PREFIX + 'Service'
|
||||
PROMPT_IFACE = SS_PREFIX + 'Prompt'
|
||||
|
||||
|
||||
class DBusAddressWrapper(DBusAddress): # type: ignore
|
||||
"""A wrapper class around :class:`jeepney.wrappers.DBusAddress`
|
||||
that adds some additional methods for calling and working with
|
||||
properties, and converts error responses to SecretStorage
|
||||
exceptions.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
def __init__(self, path: str, interface: str,
|
||||
connection: DBusConnection) -> None:
|
||||
DBusAddress.__init__(self, path, BUS_NAME, interface)
|
||||
self._connection = connection
|
||||
|
||||
def send_and_get_reply(self, msg: Message) -> Any:
|
||||
try:
|
||||
resp_msg: Message = self._connection.send_and_get_reply(msg)
|
||||
if resp_msg.header.message_type == MessageType.error:
|
||||
raise DBusErrorResponse(resp_msg)
|
||||
return resp_msg.body
|
||||
except DBusErrorResponse as resp:
|
||||
if resp.name in (DBUS_UNKNOWN_METHOD, DBUS_NO_SUCH_OBJECT):
|
||||
raise ItemNotFoundException('Item does not exist!') from resp
|
||||
elif resp.name in (DBUS_SERVICE_UNKNOWN, DBUS_EXEC_FAILED,
|
||||
DBUS_NO_REPLY):
|
||||
data = resp.data
|
||||
if isinstance(data, tuple):
|
||||
data = data[0]
|
||||
raise SecretServiceNotAvailableException(data) from resp
|
||||
raise
|
||||
|
||||
def call(self, method: str, signature: str, *body: Any) -> Any:
|
||||
msg = new_method_call(self, method, signature, body)
|
||||
return self.send_and_get_reply(msg)
|
||||
|
||||
def get_property(self, name: str) -> Any:
|
||||
msg = Properties(self).get(name)
|
||||
(signature, value), = self.send_and_get_reply(msg)
|
||||
return value
|
||||
|
||||
def set_property(self, name: str, signature: str, value: Any) -> None:
|
||||
msg = Properties(self).set(name, signature, value)
|
||||
self.send_and_get_reply(msg)
|
||||
|
||||
|
||||
def open_session(connection: DBusConnection) -> Session:
|
||||
"""Returns a new Secret Service session."""
|
||||
service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection)
|
||||
session = Session()
|
||||
try:
|
||||
output, result = service.call(
|
||||
'OpenSession', 'sv',
|
||||
ALGORITHM_DH,
|
||||
('ay', int_to_bytes(session.my_public_key)))
|
||||
except DBusErrorResponse as resp:
|
||||
if resp.name != DBUS_NOT_SUPPORTED:
|
||||
raise
|
||||
output, result = service.call(
|
||||
'OpenSession', 'sv',
|
||||
ALGORITHM_PLAIN,
|
||||
('s', ''))
|
||||
session.encrypted = False
|
||||
else:
|
||||
signature, value = output
|
||||
assert signature == 'ay'
|
||||
key = int.from_bytes(value, 'big')
|
||||
session.set_server_public_key(key)
|
||||
session.object_path = result
|
||||
return session
|
||||
|
||||
|
||||
def format_secret(session: Session, secret: bytes,
|
||||
content_type: str) -> Tuple[str, bytes, bytes, str]:
|
||||
"""Formats `secret` to make possible to pass it to the
|
||||
Secret Service API."""
|
||||
if isinstance(secret, str):
|
||||
secret = secret.encode('utf-8')
|
||||
elif not isinstance(secret, bytes):
|
||||
raise TypeError('secret must be bytes')
|
||||
assert session.object_path is not None
|
||||
if not session.encrypted:
|
||||
return (session.object_path, b'', secret, content_type)
|
||||
assert session.aes_key is not None
|
||||
# PKCS-7 style padding
|
||||
padding = 0x10 - (len(secret) & 0xf)
|
||||
secret += bytes((padding,) * padding)
|
||||
aes_iv = os.urandom(0x10)
|
||||
aes = algorithms.AES(session.aes_key)
|
||||
encryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).encryptor()
|
||||
encrypted_secret = encryptor.update(secret) + encryptor.finalize()
|
||||
return (
|
||||
session.object_path,
|
||||
aes_iv,
|
||||
encrypted_secret,
|
||||
content_type
|
||||
)
|
||||
|
||||
|
||||
def exec_prompt(connection: DBusConnection,
|
||||
prompt_path: str) -> Tuple[bool, List[str]]:
|
||||
"""Executes the prompt in a blocking mode.
|
||||
|
||||
:returns: a tuple; the first element is a boolean value showing
|
||||
whether the operation was dismissed, the second element
|
||||
is a list of unlocked object paths
|
||||
"""
|
||||
prompt = DBusAddressWrapper(prompt_path, PROMPT_IFACE, connection)
|
||||
rule = MatchRule(
|
||||
path=prompt_path,
|
||||
interface=PROMPT_IFACE,
|
||||
member='Completed',
|
||||
type=MessageType.signal,
|
||||
)
|
||||
with connection.filter(rule) as signals:
|
||||
prompt.call('Prompt', 's', '')
|
||||
dismissed, result = connection.recv_until_filtered(signals).body
|
||||
assert dismissed is not None
|
||||
assert result is not None
|
||||
return dismissed, result
|
||||
|
||||
|
||||
def unlock_objects(connection: DBusConnection, paths: List[str]) -> bool:
|
||||
"""Requests unlocking objects specified in `paths`.
|
||||
Returns a boolean representing whether the operation was dismissed.
|
||||
|
||||
.. versionadded:: 2.1.2"""
|
||||
service = DBusAddressWrapper(SS_PATH, SERVICE_IFACE, connection)
|
||||
unlocked_paths, prompt = service.call('Unlock', 'ao', paths)
|
||||
if len(prompt) > 1:
|
||||
dismissed, (signature, unlocked) = exec_prompt(connection, prompt)
|
||||
assert signature == 'ao'
|
||||
return dismissed
|
||||
return False
|
||||
|
||||
|
||||
def add_match_rules(connection: DBusConnection) -> None:
|
||||
"""Adds match rules for the given connection.
|
||||
|
||||
Currently it matches all messages from the Prompt interface, as the
|
||||
mock service (unlike GNOME Keyring) does not specify the signal
|
||||
destination.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
"""
|
||||
rule = MatchRule(sender=BUS_NAME, interface=PROMPT_IFACE)
|
||||
dbus = DBusAddressWrapper(path='/org/freedesktop/DBus',
|
||||
interface='org.freedesktop.DBus',
|
||||
connection=connection)
|
||||
dbus.bus_name = 'org.freedesktop.DBus'
|
||||
dbus.call('AddMatch', 's', rule.serialise())
|
||||
Reference in New Issue
Block a user