1# SecretStorage module for Python 2# Access passwords using the SecretService DBus API 3# Author: Dmitry Shachnev, 2013-2018 4# License: 3-clause BSD, see LICENSE file 5 6"""SecretStorage item contains a *secret*, some *attributes* and a 7*label* visible to user. Editing all these properties and reading the 8secret is possible only when the :doc:`collection <collection>` storing 9the item is unlocked. The collection can be unlocked using collection's 10:meth:`~secretstorage.collection.Collection.unlock` method.""" 11 12from typing import Dict, Optional 13from jeepney.io.blocking import DBusConnection 14from secretstorage.defines import SS_PREFIX 15from secretstorage.dhcrypto import Session 16from secretstorage.exceptions import LockedException, PromptDismissedException 17from secretstorage.util import DBusAddressWrapper, \ 18 exec_prompt, open_session, format_secret, unlock_objects 19from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 20from cryptography.hazmat.backends import default_backend 21 22ITEM_IFACE = SS_PREFIX + 'Item' 23 24class Item(object): 25 """Represents a secret item.""" 26 27 def __init__(self, connection: DBusConnection, 28 item_path: str, session: Optional[Session] = None) -> None: 29 self.item_path = item_path 30 self._item = DBusAddressWrapper(item_path, ITEM_IFACE, connection) 31 self._item.get_property('Label') 32 self.session = session 33 self.connection = connection 34 35 def __eq__(self, other: "DBusConnection") -> bool: 36 assert isinstance(other.item_path, str) 37 return self.item_path == other.item_path 38 39 def is_locked(self) -> bool: 40 """Returns :const:`True` if item is locked, otherwise 41 :const:`False`.""" 42 return bool(self._item.get_property('Locked')) 43 44 def ensure_not_locked(self) -> None: 45 """If collection is locked, raises 46 :exc:`~secretstorage.exceptions.LockedException`.""" 47 if self.is_locked(): 48 raise LockedException('Item is locked!') 49 50 def unlock(self) -> bool: 51 """Requests unlocking the item. Usually, this means that the 52 whole collection containing this item will be unlocked. 53 54 Returns a boolean representing whether the prompt has been 55 dismissed; that means :const:`False` on successful unlocking 56 and :const:`True` if it has been dismissed. 57 58 .. versionadded:: 2.1.2 59 60 .. versionchanged:: 3.0 61 No longer accepts the ``callback`` argument. 62 """ 63 return unlock_objects(self.connection, [self.item_path]) 64 65 def get_attributes(self) -> Dict[str, str]: 66 """Returns item attributes (dictionary).""" 67 attrs = self._item.get_property('Attributes') 68 return dict(attrs) 69 70 def set_attributes(self, attributes: Dict[str, str]) -> None: 71 """Sets item attributes to `attributes` (dictionary).""" 72 self._item.set_property('Attributes', 'a{ss}', attributes) 73 74 def get_label(self) -> str: 75 """Returns item label (unicode string).""" 76 label = self._item.get_property('Label') 77 assert isinstance(label, str) 78 return label 79 80 def set_label(self, label: str) -> None: 81 """Sets item label to `label`.""" 82 self.ensure_not_locked() 83 self._item.set_property('Label', 's', label) 84 85 def delete(self) -> None: 86 """Deletes the item.""" 87 self.ensure_not_locked() 88 prompt, = self._item.call('Delete', '') 89 if prompt != "/": 90 dismissed, _result = exec_prompt(self.connection, prompt) 91 if dismissed: 92 raise PromptDismissedException('Prompt dismissed.') 93 94 def get_secret(self) -> bytes: 95 """Returns item secret (bytestring).""" 96 self.ensure_not_locked() 97 if not self.session: 98 self.session = open_session(self.connection) 99 secret, = self._item.call('GetSecret', 'o', self.session.object_path) 100 if not self.session.encrypted: 101 return bytes(secret[2]) 102 assert self.session.aes_key is not None 103 aes = algorithms.AES(self.session.aes_key) 104 aes_iv = bytes(secret[1]) 105 decryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).decryptor() 106 encrypted_secret = secret[2] 107 padded_secret = decryptor.update(bytes(encrypted_secret)) + decryptor.finalize() 108 assert isinstance(padded_secret, bytes) 109 return padded_secret[:-padded_secret[-1]] 110 111 def get_secret_content_type(self) -> str: 112 """Returns content type of item secret (string).""" 113 self.ensure_not_locked() 114 if not self.session: 115 self.session = open_session(self.connection) 116 secret, = self._item.call('GetSecret', 'o', self.session.object_path) 117 return str(secret[3]) 118 119 def set_secret(self, secret: bytes, 120 content_type: str = 'text/plain') -> None: 121 """Sets item secret to `secret`. If `content_type` is given, 122 also sets the content type of the secret (``text/plain`` by 123 default).""" 124 self.ensure_not_locked() 125 if not self.session: 126 self.session = open_session(self.connection) 127 _secret = format_secret(self.session, secret, content_type) 128 self._item.call('SetSecret', '(oayays)', _secret) 129 130 def get_created(self) -> int: 131 """Returns UNIX timestamp (integer) representing the time 132 when the item was created. 133 134 .. versionadded:: 1.1""" 135 created = self._item.get_property('Created') 136 assert isinstance(created, int) 137 return created 138 139 def get_modified(self) -> int: 140 """Returns UNIX timestamp (integer) representing the time 141 when the item was last modified.""" 142 modified = self._item.get_property('Modified') 143 assert isinstance(modified, int) 144 return modified 145