add eartly build
This commit is contained in:
		
							
								
								
									
										0
									
								
								scripts/earthly/secretsd/secretsd/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								scripts/earthly/secretsd/secretsd/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										32
									
								
								scripts/earthly/secretsd/secretsd/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								scripts/earthly/secretsd/secretsd/__main__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import argparse | ||||
| import dbus | ||||
| import dbus.mainloop.glib | ||||
| from gi.repository import GLib | ||||
| import logging | ||||
| import os | ||||
| import xdg.BaseDirectory | ||||
|  | ||||
| from .database import SecretsDatabase | ||||
| from .service import SecretService | ||||
|  | ||||
| os.umask(0o077) | ||||
|  | ||||
| parser = argparse.ArgumentParser() | ||||
| parser.add_argument("-d", "--db-path", metavar="PATH") | ||||
| parser.add_argument("-k", "--key-location", metavar="PATH") | ||||
| args = parser.parse_args() | ||||
|  | ||||
| db_dir = xdg.BaseDirectory.save_data_path("nullroute.eu.org/secretsd") | ||||
| db_path = os.path.join(db_dir, "secrets.db") | ||||
| db_path = args.db_path or db_path | ||||
|  | ||||
| key_path = os.path.join(db_dir, "secrets.key") | ||||
| key_path = args.key_location or "file:%s" % key_path | ||||
|  | ||||
| dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) | ||||
| bus = dbus.SessionBus() | ||||
| sdb = SecretsDatabase(db_path, key_path) | ||||
| svc = SecretService(bus, sdb) | ||||
|  | ||||
| loop = GLib.MainLoop() | ||||
| loop.run() | ||||
							
								
								
									
										130
									
								
								scripts/earthly/secretsd/secretsd/collection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								scripts/earthly/secretsd/secretsd/collection.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| import dbus | ||||
| import dbus.service | ||||
| from .exception import NoSuchObjectException | ||||
| from .item import SecretServiceItemFallback | ||||
| from .util import BusObjectWithProperties, NullObject | ||||
|  | ||||
| class SecretServiceCollectionFallback(dbus.service.FallbackObject, BusObjectWithProperties): | ||||
|     ROOT = "/org/freedesktop/secrets/collection" | ||||
|     PATH = "/org/freedesktop/secrets/collection/c%d" | ||||
|  | ||||
|     def __init__(self, service, bus_path=ROOT): | ||||
|         self.service = service | ||||
|         self.bus_path = bus_path | ||||
|         super().__init__(self.service.bus, self.bus_path) | ||||
|  | ||||
|     def resolve_path(self, path): | ||||
|         if path.startswith("/org/freedesktop/secrets/aliases/"): | ||||
|             orig = path | ||||
|             alias = path[len("/org/freedesktop/secrets/aliases/"):] | ||||
|             path = self.service.db.resolve_alias(alias) | ||||
|             if not path: | ||||
|                 raise NoSuchObjectException(orig) | ||||
|         if not self.service.db.collection_exists(path): | ||||
|             raise NoSuchObjectException(path) | ||||
|         return path | ||||
|  | ||||
|     def get_items(self, path): | ||||
|         path = self.resolve_path(path) | ||||
|         items = self.service.db.find_items({"xdg:collection": path}) | ||||
|         return dbus.Array(items, "o") | ||||
|  | ||||
|     def get_label(self, path): | ||||
|         path = self.resolve_path(path) | ||||
|         meta = self.service.db.get_collection_metadata(path) | ||||
|         return dbus.String(meta[0]) | ||||
|  | ||||
|     def set_label(self, path, value): | ||||
|         path = self.resolve_path(path) | ||||
|         label = str(value) | ||||
|         self.service.db.set_collection_label(path, label) | ||||
|  | ||||
|     def get_created(self, path): | ||||
|         path = self.resolve_path(path) | ||||
|         meta = self.service.db.get_collection_metadata(path) | ||||
|         return dbus.UInt64(meta[1]) | ||||
|  | ||||
|     def get_modified(self, path): | ||||
|         path = self.resolve_path(path) | ||||
|         meta = self.service.db.get_collection_metadata(path) | ||||
|         return dbus.UInt64(meta[2]) | ||||
|  | ||||
|     INTERFACE = "org.freedesktop.Secret.Collection" | ||||
|     PROPERTIES = { | ||||
|         "Items":        (get_items,     None,       dbus.Array([], "o")), | ||||
|         "Label":        (get_label,     set_label,  dbus.String("")), | ||||
|         "Locked":       (None,          None,       dbus.Boolean(False)), | ||||
|         "Created":      (get_created,   None,       dbus.UInt64(0)), | ||||
|         "Modified":     (get_modified,  None,       dbus.UInt64(0)), | ||||
|     } | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Collection", "a{sv}(oayays)b", "oo", | ||||
|                          sender_keyword="sender", path_keyword="path", byte_arrays=True) | ||||
|     def CreateItem(self, properties, secret, replace, | ||||
|                    sender=None, path=None): | ||||
|         path = self.resolve_path(path) | ||||
|         label = properties["org.freedesktop.Secret.Item.Label"] | ||||
|         attrs = properties["org.freedesktop.Secret.Item.Attributes"] | ||||
|  | ||||
|         attrs.setdefault("xdg:collection", path) | ||||
|         attrs.setdefault("xdg:schema", "org.freedesktop.Secret.Generic") | ||||
|  | ||||
|         sec_session, sec_param, sec_ct, sec_type = secret | ||||
|         sec_session = self.service.path_objects[sec_session] | ||||
|         sec_data = sec_session.decrypt(sec_ct, sec_param) | ||||
|  | ||||
|         existing = self.service.db.find_items(attrs) if replace else [] | ||||
|         if existing: | ||||
|             bus_path = existing[0] | ||||
|             self.service.db.set_item_attributes(bus_path, attrs) | ||||
|             self.service.db.set_secret(bus_path, sec_data, sec_type) | ||||
|             self.service.send_signal(path, "org.freedesktop.Secret.Collection", | ||||
|                                            "ItemChanged", | ||||
|                                            "o", | ||||
|                                            bus_path) | ||||
|         else: | ||||
|             bus_path = self.service.make_bus_path(True, "/org/freedesktop/secrets/item/i%d") | ||||
|             self.service.db.add_item(bus_path, label, attrs, sec_data, sec_type) | ||||
|             self.service.send_signal(path, "org.freedesktop.Secret.Collection", | ||||
|                                            "ItemCreated", | ||||
|                                            "o", | ||||
|                                            bus_path) | ||||
|             self.service.send_signal(path, "org.freedesktop.DBus.Properties", | ||||
|                                            "PropertiesChanged", | ||||
|                                            "sa{sv}as", | ||||
|                                            "org.freedesktop.Secret.Collection", | ||||
|                                            {"Items": self.get_items(path)}, | ||||
|                                            []) | ||||
|  | ||||
|         return (dbus.ObjectPath(bus_path), NullObject) | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Collection", "", "o", | ||||
|                          path_keyword="path") | ||||
|     def Delete(self, path=None): | ||||
|         path = self.resolve_path(path) | ||||
|         self.service.db.delete_collection(path) | ||||
|         self.service.CollectionDeleted(path) | ||||
|         self.service.PropertiesChanged("org.freedesktop.Secret.Service", | ||||
|                                        {"Collections": self.service.get_collections()}, | ||||
|                                        []) | ||||
|         return NullObject | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Collection", "a{ss}", "ao", | ||||
|                          path_keyword="path") | ||||
|     def SearchItems(self, attributes, path=None): | ||||
|         path = self.resolve_path(path) | ||||
|         attributes["xdg:collection"] = path | ||||
|         items = self.service.db.find_items(attributes) | ||||
|         return items | ||||
|  | ||||
|     @dbus.service.signal("org.freedesktop.Secret.Collection", "o") | ||||
|     def ItemCreated(self, bus_path): | ||||
|         pass | ||||
|  | ||||
|     @dbus.service.signal("org.freedesktop.Secret.Collection", "o") | ||||
|     def ItemDeleted(self, bus_path): | ||||
|         pass | ||||
|  | ||||
|     @dbus.service.signal("org.freedesktop.Secret.Collection", "o") | ||||
|     def ItemChanged(self, bus_path): | ||||
|         pass | ||||
							
								
								
									
										132
									
								
								scripts/earthly/secretsd/secretsd/crypto_backend.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								scripts/earthly/secretsd/secretsd/crypto_backend.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| import os | ||||
|  | ||||
| __all__ = [ | ||||
|     "AES_BLOCK_BYTES", | ||||
|     "aes_cbc_encrypt", | ||||
|     "aes_cbc_decrypt", | ||||
|     "aes_cfb8_encrypt", | ||||
|     "aes_cfb8_decrypt", | ||||
|     "dh_modp1024_exchange", | ||||
|     "hkdf_sha256_derive", | ||||
|     "pkcs7_pad", | ||||
|     "pkcs7_unpad", | ||||
| ] | ||||
|  | ||||
| # Second Oakley group (RFC 2409), to be used as "dh-ietf1024" in the 'Secret | ||||
| # Service' API. Don't look at me. I didn't write the spec. | ||||
| MODP1024_PRIME = int("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" | ||||
|                      "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" | ||||
|                      "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" | ||||
|                      "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" | ||||
|                      "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" | ||||
|                      "FFFFFFFFFFFFFFFF", 16) | ||||
| MODP1024_GENERATOR = 2 | ||||
|  | ||||
| backend = os.environ.get("CRYPTO_BACKEND", "cryptography") | ||||
|  | ||||
| if backend == "cryptodome": | ||||
|     from Crypto.Cipher import AES | ||||
|     from Crypto.Hash import SHA256 | ||||
|     from Crypto.Protocol.KDF import HKDF | ||||
|     from Crypto.Random.random import randint | ||||
|     from Crypto.Util import Padding | ||||
|  | ||||
|     AES_BLOCK_BYTES = AES.block_size | ||||
|  | ||||
|     def aes_cbc_encrypt(data, key, iv): | ||||
|         return AES.new(key, AES.MODE_CBC, iv).encrypt(data) | ||||
|  | ||||
|     def aes_cbc_decrypt(data, key, iv): | ||||
|         return AES.new(key, AES.MODE_CBC, iv).decrypt(data) | ||||
|  | ||||
|     def aes_cfb8_encrypt(data, key, iv): | ||||
|         return AES.new(key, AES.MODE_CFB, iv, segment_size=8).encrypt(data) | ||||
|  | ||||
|     def aes_cfb8_decrypt(data, key, iv): | ||||
|         return AES.new(key, AES.MODE_CFB, iv, segment_size=8).decrypt(data) | ||||
|  | ||||
|     def aes_cfb128_encrypt(data, key, iv): | ||||
|         return AES.new(key, AES.MODE_CFB, iv, segment_size=128).encrypt(data) | ||||
|  | ||||
|     def aes_cfb128_decrypt(data, key, iv): | ||||
|         return AES.new(key, AES.MODE_CFB, iv, segment_size=128).decrypt(data) | ||||
|  | ||||
|     def dh_modp1024_exchange(peer_pubkey): | ||||
|         prime = MODP1024_PRIME | ||||
|         generator = MODP1024_GENERATOR | ||||
|         our_privkey = randint(1, prime-1) | ||||
|         our_pubkey = pow(generator, our_privkey, prime) | ||||
|         shared_key = pow(peer_pubkey, our_privkey, prime) | ||||
|         shared_key = shared_key.to_bytes(1024 // 8, "big") | ||||
|         return our_pubkey, shared_key | ||||
|  | ||||
|     def hkdf_sha256_derive(input, nbytes): | ||||
|         return HKDF(input, nbytes, b"", hashmod=SHA256) | ||||
|  | ||||
|     def pkcs7_pad(data, size): | ||||
|         return Padding.pad(data, size, style="pkcs7") | ||||
|  | ||||
|     def pkcs7_unpad(data, size): | ||||
|         return Padding.unpad(data, size, style="pkcs7") | ||||
|  | ||||
|     print("using 'PyCryptodome' as crypto backend") | ||||
|  | ||||
| elif backend == "cryptography": | ||||
|     from cryptography.hazmat.primitives.asymmetric import dh | ||||
|     from cryptography.hazmat.primitives.ciphers import Cipher | ||||
|     from cryptography.hazmat.primitives.ciphers.algorithms import AES | ||||
|     from cryptography.hazmat.primitives.ciphers.modes import CBC, CFB, CFB8 | ||||
|     from cryptography.hazmat.primitives.hashes import SHA256 | ||||
|     from cryptography.hazmat.primitives.kdf.hkdf import HKDF | ||||
|     from cryptography.hazmat.primitives.padding import PKCS7 | ||||
|  | ||||
|     AES_BLOCK_BYTES = AES.block_size // 8 | ||||
|  | ||||
|     def aes_cbc_encrypt(data, key, iv): | ||||
|         c = Cipher(AES(key), CBC(iv)).encryptor() | ||||
|         return c.update(data) + c.finalize() | ||||
|  | ||||
|     def aes_cbc_decrypt(data, key, iv): | ||||
|         c = Cipher(AES(key), CBC(iv)).decryptor() | ||||
|         return c.update(data) + c.finalize() | ||||
|  | ||||
|     def aes_cfb8_encrypt(data, key, iv): | ||||
|         c = Cipher(AES(key), CFB8(iv)).encryptor() | ||||
|         return c.update(data) + c.finalize() | ||||
|  | ||||
|     def aes_cfb8_decrypt(data, key, iv): | ||||
|         c = Cipher(AES(key), CFB8(iv)).decryptor() | ||||
|         return c.update(data) + c.finalize() | ||||
|  | ||||
|     def aes_cfb128_encrypt(data, key, iv): | ||||
|         c = Cipher(AES(key), CFB(iv)).encryptor() | ||||
|         return c.update(data) + c.finalize() | ||||
|  | ||||
|     def aes_cfb128_decrypt(data, key, iv): | ||||
|         c = Cipher(AES(key), CFB(iv)).decryptor() | ||||
|         return c.update(data) + c.finalize() | ||||
|  | ||||
|     def dh_modp1024_exchange(peer_pubkey): | ||||
|         group = dh.DHParameterNumbers(p=MODP1024_PRIME, g=MODP1024_GENERATOR) | ||||
|         peer_pubkey = dh.DHPublicNumbers(peer_pubkey, group).public_key() | ||||
|         our_privkey = group.parameters().generate_private_key() | ||||
|         shared_key = our_privkey.exchange(peer_pubkey) | ||||
|         our_pubkey = our_privkey.public_key().public_numbers().y | ||||
|         return our_pubkey, shared_key | ||||
|  | ||||
|     def hkdf_sha256_derive(input, nbytes): | ||||
|         k = HKDF(algorithm=SHA256(), length=nbytes, salt=b"", info=b"") | ||||
|         return k.derive(input) | ||||
|  | ||||
|     def pkcs7_pad(data, size): | ||||
|         p = PKCS7(size * 8).padder() | ||||
|         return p.update(data) + p.finalize() | ||||
|  | ||||
|     def pkcs7_unpad(data, size): | ||||
|         p = PKCS7(size * 8).unpadder() | ||||
|         return p.update(data) + p.finalize() | ||||
|  | ||||
|     print("using 'python-cryptography' as crypto backend") | ||||
|  | ||||
| else: | ||||
|     raise RuntimeError("unsupported crypto backend %r" % backend) | ||||
							
								
								
									
										377
									
								
								scripts/earthly/secretsd/secretsd/database.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										377
									
								
								scripts/earthly/secretsd/secretsd/database.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,377 @@ | ||||
| import base64 | ||||
| import sqlite3 | ||||
| import time | ||||
|  | ||||
| from .encryption import (generate_key, | ||||
|                          aes_cfb8_wrap, aes_cfb8_unwrap, | ||||
|                          aes_cfb128_wrap, aes_cfb128_unwrap) | ||||
| from .external_keys import load_ext_key, store_ext_key | ||||
|  | ||||
| class SecretsDatabase(): | ||||
|     def __init__(self, path, key_path): | ||||
|         self.db = sqlite3.connect(path) | ||||
|         self.kp = key_path | ||||
|         self.mk = None | ||||
|         self.dk = None | ||||
|         self.ver = 0 | ||||
|         self.initialize() | ||||
|         self.upgrade() | ||||
|         self.load_keys() | ||||
|  | ||||
|     def initialize(self): | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("CREATE TABLE IF NOT EXISTS version (" \ | ||||
|                     "   version INTEGER" \ | ||||
|                     ")") | ||||
|         cur.execute("CREATE TABLE IF NOT EXISTS sequence (" \ | ||||
|                     "   next INTEGER" \ | ||||
|                     ")") | ||||
|         cur.execute("CREATE TABLE IF NOT EXISTS parameters (" \ | ||||
|                     "   name TEXT," \ | ||||
|                     "   value TEXT" \ | ||||
|                     ")") | ||||
|         cur.execute("CREATE TABLE IF NOT EXISTS collections (" \ | ||||
|                     "   object TEXT," \ | ||||
|                     "   label TEXT," \ | ||||
|                     "   created INTEGER," \ | ||||
|                     "   modified INTEGER" \ | ||||
|                     ")") | ||||
|         cur.execute("CREATE TABLE IF NOT EXISTS aliases (" \ | ||||
|                     "   alias TEXT," \ | ||||
|                     "   target TEXT" \ | ||||
|                     ")") | ||||
|         cur.execute("CREATE TABLE IF NOT EXISTS items (" \ | ||||
|                     "   object TEXT," \ | ||||
|                     "   label TEXT," \ | ||||
|                     "   created INTEGER," \ | ||||
|                     "   modified INTEGER" \ | ||||
|                     ")") | ||||
|         cur.execute("CREATE TABLE IF NOT EXISTS attributes (" \ | ||||
|                     "   object TEXT," \ | ||||
|                     "   attribute TEXT," \ | ||||
|                     "   value TEXT" \ | ||||
|                     ")") | ||||
|         cur.execute("CREATE TABLE IF NOT EXISTS secrets (" \ | ||||
|                     "   object TEXT," \ | ||||
|                     "   secret TEXT," \ | ||||
|                     "   type TEXT" \ | ||||
|                     ")") | ||||
|         self.db.commit() | ||||
|  | ||||
|     # Encryption keys | ||||
|  | ||||
|     def _store_mkey(self, key): | ||||
|         print("DB: storing master key to %r" % (self.kp)) | ||||
|         store_ext_key(self.kp, base64.b64encode(key).decode()) | ||||
|  | ||||
|     def _load_mkey(self): | ||||
|         print("DB: loading master key from %r" % (self.kp)) | ||||
|         try: | ||||
|             mkey = base64.b64decode(load_ext_key(self.kp)) | ||||
|             if len(mkey) != 32: | ||||
|                 raise IOError("wrong mkey length (expected 32 bytes)") | ||||
|         except (KeyError, FileNotFoundError): | ||||
|             raise RuntimeError("could not load the database key from %r" % (self.kp)) | ||||
|         self.mk = mkey | ||||
|  | ||||
|     def _load_dkey(self, *, v=0): | ||||
|         if (v or self.ver) == 3: | ||||
|             cur = self.db.cursor() | ||||
|             cur.execute("SELECT value FROM parameters WHERE name = 'dkey'") | ||||
|             dkey, = cur.fetchone() | ||||
|             try: | ||||
|                 dkey = self._decrypt_buf(dkey, with_mkey=True, v=3) | ||||
|             except IOError as e: | ||||
|                 raise IOError("wrong mkey (%s)" % e) | ||||
|             if len(dkey) != 32: | ||||
|                 raise IOError("wrong dkey length (expected 32 bytes)") | ||||
|             self.dk = dkey | ||||
|         else: | ||||
|             raise NotImplementedError("unknown schema version %r" % (v or self.ver)) | ||||
|  | ||||
|     def load_keys(self): | ||||
|         if self.ver >= 2: | ||||
|             self._load_mkey() | ||||
|             self._load_dkey() | ||||
|  | ||||
|     def _encrypt_buf(self, buf, *, with_mkey=False, v=0): | ||||
|         key = self.mk if with_mkey else self.dk | ||||
|         if (v or self.ver) >= 3: | ||||
|             return aes_cfb128_wrap(buf, key) | ||||
|         elif (v or self.ver) == 2: | ||||
|             return aes_cfb8_wrap(buf, key) | ||||
|         else: | ||||
|             raise NotImplementedError("unknown schema version %r" % (v or self.ver)) | ||||
|  | ||||
|     def _decrypt_buf(self, buf, *, with_mkey=None, v=0): | ||||
|         key = self.mk if with_mkey else self.dk | ||||
|         if (v or self.ver) >= 3: | ||||
|             return aes_cfb128_unwrap(buf, key) | ||||
|         elif (v or self.ver) == 2: | ||||
|             return aes_cfb8_unwrap(buf, key) | ||||
|         else: | ||||
|             raise NotImplementedError("unknown schema version %r" % (v or self.ver)) | ||||
|  | ||||
|     # Schema upgrades | ||||
|  | ||||
|     def _upgrade_v0_to_v1(self): | ||||
|         # Undo commit affc514 "make items use bus paths underneath their collection" | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT object FROM items" \ | ||||
|                     " WHERE object LIKE '/org/freedesktop/secrets/collection/c%/i%'") | ||||
|         res = cur.fetchall() | ||||
|         for (old_object,) in res: | ||||
|             item_id = old_object.split("/")[-1] | ||||
|             new_object = "/org/freedesktop/secrets/item/%s" % item_id | ||||
|             print("DB: moving object %r => %r" % (old_object, new_object)) | ||||
|             cur.execute("UPDATE items      SET object = ? WHERE object = ?", | ||||
|                         (new_object, old_object)) | ||||
|             cur.execute("UPDATE secrets    SET object = ? WHERE object = ?", | ||||
|                         (new_object, old_object)) | ||||
|             cur.execute("UPDATE attributes SET object = ? WHERE object = ?", | ||||
|                         (new_object, old_object)) | ||||
|  | ||||
|     def _upgrade_v1_to_v3(self): | ||||
|         # Version 2 encrypts all secrets using the database master key | ||||
|         cur = self.db.cursor() | ||||
|         # Generate a "master key" | ||||
|         print("DB: generating a master key") | ||||
|         mkey = generate_key() | ||||
|         self._store_mkey(mkey) | ||||
|         self.mk = mkey | ||||
|         # Generate a "data key" | ||||
|         print("DB: generating a data key") | ||||
|         dkey = generate_key() | ||||
|         blob = self._encrypt_buf(dkey, with_mkey=True, v=3) | ||||
|         cur.execute("INSERT INTO parameters VALUES ('dkey', ?)", (blob,)) | ||||
|         self.dk = dkey | ||||
|         # Encrypt all currently stored secrets | ||||
|         cur.execute("SELECT object, secret FROM secrets") | ||||
|         res = cur.fetchall() | ||||
|         for object, blob in res: | ||||
|             print("DB: encrypting secret %r" % (object,)) | ||||
|             blob = self._encrypt_buf(blob, v=3) | ||||
|             cur.execute("UPDATE secrets SET secret = ? WHERE object = ?", (blob, object)) | ||||
|  | ||||
|     def _upgrade_v2_to_v3(self): | ||||
|         # Version 3 uses AES-CFB128 instead of (badly chosen) AES-CFB8 | ||||
|         cur = self.db.cursor() | ||||
|         # Re-encrypt the data key | ||||
|         self._load_mkey() | ||||
|         cur.execute("SELECT value FROM parameters WHERE name = 'dkey'") | ||||
|         blob, = cur.fetchone() | ||||
|         blob = self._decrypt_buf(blob, with_mkey=True, v=2) | ||||
|         blob = self._encrypt_buf(blob, with_mkey=True, v=3) | ||||
|         cur.execute("UPDATE parameters SET value = ? WHERE name = 'dkey'", (blob,)) | ||||
|         # Re-encrypt all currently stored secrets | ||||
|         self._load_dkey(v=3) | ||||
|         cur.execute("SELECT object, secret FROM secrets") | ||||
|         res = cur.fetchall() | ||||
|         for object, blob in res: | ||||
|             print("DB: re-encrypting secret %r" % (object,)) | ||||
|             blob = self._decrypt_buf(blob, v=2) | ||||
|             blob = self._encrypt_buf(blob, v=3) | ||||
|             cur.execute("UPDATE secrets SET secret = ? WHERE object = ?", (blob, object)) | ||||
|  | ||||
|     def upgrade(self): | ||||
|         print("DB: current database version is %d" % self.get_version()) | ||||
|         if self.get_version() == 0: | ||||
|             print("DB: upgrading to version %d" % (1,)) | ||||
|             self._upgrade_v0_to_v1() | ||||
|             self.db.cursor().execute("UPDATE version SET version = ?", (1,)) | ||||
|             self.db.commit() | ||||
|         if self.get_version() == 1: | ||||
|             print("DB: upgrading to version %d" % (3,)) | ||||
|             self._upgrade_v1_to_v3() | ||||
|             self.db.cursor().execute("UPDATE version SET version = ?", (3,)) | ||||
|             self.db.commit() | ||||
|             print("DB: vacuuming database") | ||||
|             self.db.cursor().execute("VACUUM") | ||||
|         if self.get_version() == 2: | ||||
|             print("DB: upgrading to version %d" % (3,)) | ||||
|             self._upgrade_v2_to_v3() | ||||
|             self.db.cursor().execute("UPDATE version SET version = ?", (3,)) | ||||
|             self.db.commit() | ||||
|         self.ver = self.get_version() | ||||
|         print("DB: new database version is %d" % self.ver) | ||||
|  | ||||
|     def get_version(self): | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT version FROM version") | ||||
|         res = cur.fetchone() | ||||
|         if res: | ||||
|             version = res[0] | ||||
|         else: | ||||
|             version = 0 | ||||
|             cur.execute("INSERT INTO version VALUES (?)", (version,)) | ||||
|             self.db.commit() | ||||
|         return version | ||||
|  | ||||
|     def get_next_object_id(self): | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT next FROM sequence") | ||||
|         res = cur.fetchone() | ||||
|         if res: | ||||
|             oid = res[0] | ||||
|             cur.execute("UPDATE sequence SET next = next + 1") | ||||
|         else: | ||||
|             oid = 0 | ||||
|             cur.execute("INSERT INTO sequence VALUES (?)", (oid + 1,)) | ||||
|         self.db.commit() | ||||
|         print("DB: allocated new object ID %r" % oid) | ||||
|         return oid | ||||
|  | ||||
|     # Collections | ||||
|  | ||||
|     def add_collection(self, object, label): | ||||
|         print("DB: adding collection %r with label %r" % (object, label)) | ||||
|         now = int(time.time()) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("INSERT INTO collections VALUES (?,?,?,?)", (object, label, now, now)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     def list_collections(self): | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT object FROM collections") | ||||
|         return [r[0] for r in cur.fetchall()] | ||||
|  | ||||
|     def collection_exists(self, object): | ||||
|         return bool(self.get_collection_metadata(object)) | ||||
|  | ||||
|     def get_collection_metadata(self, object): | ||||
|         print("DB: getting collection metadata for %r" % (object,)) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT label, created, modified FROM collections WHERE object = ?", | ||||
|                     (object,)) | ||||
|         return cur.fetchone() | ||||
|  | ||||
|     def set_collection_label(self, object, label): | ||||
|         print("DB: setting label for %r to %r" % (object, label)) | ||||
|         now = int(time.time()) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("UPDATE collections SET label = ?, modified = ? WHERE object = ?", | ||||
|                     (label, now, object)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     def delete_collection(self, object): | ||||
|         print("DB: deleting collection %r" % (object,)) | ||||
|         cur = self.db.cursor() | ||||
|         subquery = "SELECT object FROM attributes" \ | ||||
|                    " WHERE attribute = 'xdg:collection' AND value = ?" | ||||
|         cur.execute("DELETE FROM items WHERE object IN (" + subquery + ")", (object,)) | ||||
|         cur.execute("DELETE FROM secrets WHERE object IN (" + subquery + ")", (object,)) | ||||
|         cur.execute("DELETE FROM attributes WHERE object IN (" + subquery + ")", (object,)) | ||||
|         cur.execute("DELETE FROM aliases WHERE target = ?", (object,)) | ||||
|         cur.execute("DELETE FROM collections WHERE object = ?", (object,)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     # Aliases | ||||
|  | ||||
|     def add_alias(self, alias, target): | ||||
|         print("DB: adding alias %r -> %r" % (alias, target)) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("DELETE FROM aliases WHERE alias = ?", (alias,)) | ||||
|         cur.execute("INSERT INTO aliases VALUES (?,?)", (alias, target)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     def get_aliases(self): | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT alias, target FROM aliases") | ||||
|         return cur.fetchall() | ||||
|  | ||||
|     def resolve_alias(self, alias): | ||||
|         print("DB: resolving alias %r" % (alias,)) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT target FROM aliases WHERE alias = ?", (alias,)) | ||||
|         r = cur.fetchone() | ||||
|         return r[0] if r else None | ||||
|  | ||||
|     def delete_alias(self, alias): | ||||
|         print("DB: deleting alias %r" % (alias,)) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("DELETE FROM aliases WHERE alias = ?", (alias,)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     # Items | ||||
|  | ||||
|     def add_item(self, object, label, attrs, secret, sec_type): | ||||
|         now = int(time.time()) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("INSERT INTO items VALUES (?,?,?,?)", (object, label, now, now)) | ||||
|         for key, val in attrs.items(): | ||||
|             cur.execute("INSERT INTO attributes VALUES (?,?,?)", (object, key, val)) | ||||
|         cur.execute("INSERT INTO secrets VALUES (?,?,?)", (object, self._encrypt_buf(secret), | ||||
|                                                            sec_type)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     def find_items(self, match_attrs): | ||||
|         qry = "SELECT object FROM attributes WHERE attribute = ? AND value = ?" | ||||
|         qry = " INTERSECT ".join([qry] * len(match_attrs)) | ||||
|         parvs = [] | ||||
|         for k, v in match_attrs.items(): | ||||
|             parvs += [k, v] | ||||
|         print("DB: searching for %r" % parvs) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute(qry, parvs) | ||||
|         return [r[0] for r in cur.fetchall()] | ||||
|  | ||||
|     def item_exists(self, object): | ||||
|         return bool(self.get_item_metadata(object)) | ||||
|  | ||||
|     def get_item_metadata(self, object): | ||||
|         print("DB: getting metadata for %r" % object) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT label, created, modified FROM items WHERE object = ?", | ||||
|                     (object,)) | ||||
|         return cur.fetchone() | ||||
|  | ||||
|     def set_item_label(self, object, label): | ||||
|         print("DB: setting label for %r to %r" % (object, label)) | ||||
|         now = int(time.time()) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("UPDATE items SET label = ?, modified = ? WHERE object = ?", | ||||
|                     (label, now, object)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     def get_item_attributes(self, object): | ||||
|         print("DB: getting attrs for %r" % object) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT attribute, value FROM attributes WHERE object = ?", (object,)) | ||||
|         return {k: v for k, v in cur.fetchall()} | ||||
|  | ||||
|     def set_item_attributes(self, object, attrs): | ||||
|         print("DB: setting attrs for %r to %r" % (object, attrs)) | ||||
|         now = int(time.time()) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("DELETE FROM attributes WHERE object = ?", (object,)) | ||||
|         for key, val in attrs.items(): | ||||
|             cur.execute("INSERT INTO attributes VALUES (?,?,?)", (object, key, val)) | ||||
|         cur.execute("UPDATE items SET modified = ? WHERE object = ?", (now, object)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     def get_secret(self, object): | ||||
|         print("DB: getting secret for %r" % object) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("SELECT secret, type FROM secrets WHERE object = ?", (object,)) | ||||
|         secret, sec_type = cur.fetchone() | ||||
|         return self._decrypt_buf(secret), sec_type | ||||
|  | ||||
|     def set_secret(self, object, secret, sec_type): | ||||
|         print("DB: updating secret for %r" % object) | ||||
|         if hasattr(secret, "encode"): | ||||
|             raise ValueError("secret needs to be bytes, not str") | ||||
|         now = int(time.time()) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("UPDATE secrets SET secret = ?, type = ? WHERE object = ?", | ||||
|                     (self._encrypt_buf(secret), sec_type, object)) | ||||
|         cur.execute("UPDATE items SET modified = ? WHERE object = ?", | ||||
|                     (now, object)) | ||||
|         self.db.commit() | ||||
|  | ||||
|     def delete_item(self, object): | ||||
|         print("DB: deleting item %r" % object) | ||||
|         cur = self.db.cursor() | ||||
|         cur.execute("DELETE FROM attributes WHERE object = ?", (object,)) | ||||
|         cur.execute("DELETE FROM secrets WHERE object = ?", (object,)) | ||||
|         cur.execute("DELETE FROM items WHERE object = ?", (object,)) | ||||
|         self.db.commit() | ||||
							
								
								
									
										55
									
								
								scripts/earthly/secretsd/secretsd/encryption.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								scripts/earthly/secretsd/secretsd/encryption.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import base64 | ||||
| import hmac | ||||
| import os | ||||
|  | ||||
| from .crypto_backend import ( | ||||
|     AES_BLOCK_BYTES, | ||||
|     aes_cfb8_encrypt, | ||||
|     aes_cfb8_decrypt, | ||||
|     aes_cfb128_encrypt, | ||||
|     aes_cfb128_decrypt, | ||||
| ) | ||||
|  | ||||
| KEY_SIZE_BYTES = 32 | ||||
|  | ||||
| SHA256_HMAC_BYTES = 32 | ||||
|  | ||||
| def _xor_bytes(a, b): | ||||
|     assert(len(a) == len(b)) | ||||
|     return bytes([ax ^ bx for ax, bx in zip(a, b)]) | ||||
|  | ||||
| def _fold_key(buf): | ||||
|     assert(len(buf) == 32) | ||||
|     return _xor_bytes(buf[:16], buf[16:]) | ||||
|  | ||||
| def generate_key(): | ||||
|     return os.urandom(KEY_SIZE_BYTES) | ||||
|  | ||||
| def sha256_hmac(buf, key): | ||||
|     return hmac.new(key, buf, digestmod="sha256").digest() | ||||
|  | ||||
| def aes_cfb8_wrap(data, key): | ||||
|     iv = os.urandom(AES_BLOCK_BYTES) | ||||
|     ct = aes_cfb8_encrypt(data, key, iv) | ||||
|     buf = iv + ct | ||||
|     return sha256_hmac(buf, key) + buf | ||||
|  | ||||
| def aes_cfb8_unwrap(buf, key): | ||||
|     mac, buf = buf[:SHA256_HMAC_BYTES], buf[SHA256_HMAC_BYTES:] | ||||
|     if sha256_hmac(buf, key) != mac: | ||||
|         raise IOError("MAC verification failed") | ||||
|     iv, ct = buf[:AES_BLOCK_BYTES], buf[AES_BLOCK_BYTES:] | ||||
|     return aes_cfb8_decrypt(ct, key, iv) | ||||
|  | ||||
| def aes_cfb128_wrap(data, key): | ||||
|     iv = os.urandom(AES_BLOCK_BYTES) | ||||
|     ct = aes_cfb128_encrypt(data, key, iv) | ||||
|     buf = iv + ct | ||||
|     return sha256_hmac(buf, key) + buf | ||||
|  | ||||
| def aes_cfb128_unwrap(buf, key): | ||||
|     mac, buf = buf[:SHA256_HMAC_BYTES], buf[SHA256_HMAC_BYTES:] | ||||
|     if sha256_hmac(buf, key) != mac: | ||||
|         raise IOError("MAC verification failed") | ||||
|     iv, ct = buf[:AES_BLOCK_BYTES], buf[AES_BLOCK_BYTES:] | ||||
|     return aes_cfb128_decrypt(ct, key, iv) | ||||
							
								
								
									
										18
									
								
								scripts/earthly/secretsd/secretsd/exception.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								scripts/earthly/secretsd/secretsd/exception.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import dbus | ||||
|  | ||||
| class InvalidArgsException(dbus.DBusException): | ||||
|     _dbus_error_name = "org.freedesktop.DBus.Error.InvalidArgs" | ||||
|  | ||||
| class NotYetImplementedException(dbus.DBusException): | ||||
|     _dbus_error_name = "org.freedesktop.DBus.Error.NotSupported" | ||||
|     def __init__(self): | ||||
|         super().__init__("TODO: Not implemented") | ||||
|  | ||||
| class IsLockedException(dbus.DBusException): | ||||
|     _dbus_error_name = "org.freedesktop.Secret.Error.IsLocked" | ||||
|  | ||||
| class NoSessionException(dbus.DBusException): | ||||
|     _dbus_error_name = "org.freedesktop.Secret.Error.NoSession" | ||||
|  | ||||
| class NoSuchObjectException(dbus.DBusException): | ||||
|     _dbus_error_name = "org.freedesktop.Secret.Error.NoSuchObject" | ||||
							
								
								
									
										106
									
								
								scripts/earthly/secretsd/secretsd/external_keys.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								scripts/earthly/secretsd/secretsd/external_keys.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| # This module can load passwords from external sources, such as files or | ||||
| # environment variables or KWallet (and no, *not* libsecret -- as much as I | ||||
| # want to use the nifty gi-based libsecret API...). | ||||
| # | ||||
| # It deals with strings (the lowest common denominator), but it's really meant | ||||
| # to be used for storing Base64-encoded keys and not raw passwords. | ||||
|  | ||||
| import dbus | ||||
| import os | ||||
| import re | ||||
| import subprocess | ||||
|  | ||||
| class KWalletClient(): | ||||
|     app_id = "org.eu.nullroute.Secretsd" | ||||
|     folder = "Passwords" | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.bus = dbus.SessionBus() | ||||
|         self.mgr = self.bus.get_object("org.kde.kwalletd5", "/modules/kwalletd5") | ||||
|         self.mgr = dbus.Interface(self.mgr, "org.kde.KWallet") | ||||
|  | ||||
|     def __enter__(self): | ||||
|         self.wallet = self.mgr.localWallet() | ||||
|         self.handle = self.mgr.open(self.wallet, 0, self.app_id) | ||||
|         return self | ||||
|  | ||||
|     def __exit__(self, *argv): | ||||
|         self.mgr.disconnectApplication(self.wallet, self.app_id) | ||||
|  | ||||
|     def get_password(self, name): | ||||
|         if self.mgr.hasEntry(self.handle, self.folder, name, self.app_id): | ||||
|             return str(self.mgr.readPassword(self.handle, self.folder, name, self.app_id)) | ||||
|         else: | ||||
|             raise KeyError(name) | ||||
|  | ||||
|     def set_password(self, name, value): | ||||
|         self.mgr.writePassword(self.handle, self.folder, name, value, self.app_id) | ||||
|  | ||||
| def _parse_specifier(source): | ||||
|     m = re.match(r"^(\w+):(.*)", source) | ||||
|     if m: | ||||
|         return m.groups() | ||||
|     else: | ||||
|         # Too easy to end up saving keys in a file named 'kwallet'... | ||||
|         #return "file", source | ||||
|         raise ValueError("key location must be specified as 'type:rest'") | ||||
|  | ||||
| def load_ext_key(source): | ||||
|     kind, rest = _parse_specifier(source) | ||||
|     if kind == "env": | ||||
|         return os.environ[rest] | ||||
|     elif kind == "exec": | ||||
|         res = subprocess.run(rest, shell=True, | ||||
|                                    stdin=subprocess.DEVNULL, | ||||
|                                    stdout=subprocess.PIPE, | ||||
|                                    check=True) | ||||
|         return res.stdout.decode().strip() | ||||
|     elif kind == "file": | ||||
|         with open(rest, "r") as fh: | ||||
|             return fh.read().strip() | ||||
|     elif kind == "kwallet": | ||||
|         with KWalletClient() as kw: | ||||
|             return kw.get_password(rest or "secretsd master key") | ||||
|     elif kind == "libsecret": | ||||
|         raise ValueError("cannot load external key from myself") | ||||
|     else: | ||||
|         raise ValueError("unknown external key source %r" % kind) | ||||
|  | ||||
| def store_ext_key(source, key): | ||||
|     kind, rest = _parse_specifier(source) | ||||
|     if kind == "env": | ||||
|         raise ValueError("environment is volatile storage, cannot store keys there") | ||||
|     elif kind == "exec": | ||||
|         # XXX: Should there be a way to distinguish whether the command is | ||||
|         # invoked for load vs store, or should I just remove storing to exec: | ||||
|         # because this is an advanced operation and the user can just emulate | ||||
|         # it with a temporary file? | ||||
|         res = subprocess.run(rest, shell=True, | ||||
|                                    input=key.encode(), | ||||
|                                    check=True) | ||||
|     elif kind == "file": | ||||
|         with open(rest, "w", opener=lambda p, f: os.open(p, f, 0o400)) as fh: | ||||
|             fh.write(key) | ||||
|     elif kind == "kwallet": | ||||
|         with KWalletClient() as kw: | ||||
|             kw.set_password(rest or "secretsd master key", key) | ||||
|     elif kind == "libsecret": | ||||
|         raise ValueError("cannot store external key in myself") | ||||
|     else: | ||||
|         raise ValueError("unknown external key source %r" % kind) | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     import argparse | ||||
|  | ||||
|     parser = argparse.ArgumentParser() | ||||
|     parser.add_argument("specifier") | ||||
|     parser.add_argument("--store-key", metavar="KEY", help="store the specified key (as text)") | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     arg = args.specifier | ||||
|     if key := args.store_key: | ||||
|         print(f"Storing key {key!r} to {arg!r}") | ||||
|         store_ext_key(arg, key) | ||||
|     print(f"Retrieving key from {arg!r}") | ||||
|     key = load_ext_key(arg) | ||||
|     print(f"The key is {key!r}") | ||||
							
								
								
									
										96
									
								
								scripts/earthly/secretsd/secretsd/item.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								scripts/earthly/secretsd/secretsd/item.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| import dbus | ||||
| import dbus.service | ||||
| from .util import BusObjectWithProperties, NullObject | ||||
|  | ||||
| class SecretServiceItemFallback(dbus.service.FallbackObject, BusObjectWithProperties): | ||||
|     ROOT = "/org/freedesktop/secrets/item" | ||||
|     PATH = "/org/freedesktop/secrets/item/i%d" | ||||
|  | ||||
|     def __init__(self, service, bus_path=ROOT): | ||||
|         self.service = service | ||||
|         self.bus_path = bus_path | ||||
|         super().__init__(self.service.bus, self.bus_path) | ||||
|  | ||||
|     def get_collection(self, path): | ||||
|         attrs = self.service.db.get_item_attributes(path) | ||||
|         if attrs is None: | ||||
|             raise NoSuchObjectException(path) | ||||
|         return attrs["xdg:collection"] | ||||
|  | ||||
|     def get_attributes(self, path): | ||||
|         attrs = self.service.db.get_item_attributes(path) | ||||
|         if attrs is None: | ||||
|             raise NoSuchObjectException(path) | ||||
|         attrs.setdefault("xdg:schema", "org.freedesktop.Secret.Generic") | ||||
|         return attrs | ||||
|  | ||||
|     def set_attributes(self, path, value): | ||||
|         if not self.service.db.item_exists(path): | ||||
|             raise NoSuchObjectException(path) | ||||
|         attrs["xdg:collection"] = self.get_collection(path) | ||||
|         self.service.db.set_item_attributes(path, value) | ||||
|  | ||||
|     def get_label(self, path): | ||||
|         meta = self.service.db.get_item_metadata(path) | ||||
|         if not meta: | ||||
|             raise NoSuchObjectException(path) | ||||
|         return meta[0] | ||||
|  | ||||
|     def set_label(self, path, value): | ||||
|         if not self.service.db.item_exists(path): | ||||
|             raise NoSuchObjectException(path) | ||||
|         self.service.db.set_item_label(path, value) | ||||
|  | ||||
|     def get_created(self, path): | ||||
|         meta = self.service.db.get_item_metadata(path) | ||||
|         if not meta: | ||||
|             raise NoSuchObjectException(path) | ||||
|         return dbus.UInt64(meta[1]) | ||||
|  | ||||
|     def get_modified(self, path): | ||||
|         meta = self.service.db.get_item_metadata(path) | ||||
|         if not meta: | ||||
|             raise NoSuchObjectException(path) | ||||
|         return dbus.UInt64(meta[2]) | ||||
|  | ||||
|     INTERFACE = "org.freedesktop.Secret.Item" | ||||
|     PROPERTIES = { | ||||
|         "Attributes":   (get_attributes,    set_attributes, None), | ||||
|         "Created":      (get_created,       None,           None), | ||||
|         "Label":        (get_label,         set_label,      None), | ||||
|         "Locked":       (None,              None,           False), | ||||
|         "Modified":     (get_modified,      None,           None), | ||||
|     } | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Item", "", "o", | ||||
|                          path_keyword="path") | ||||
|     def Delete(self, path=None): | ||||
|         coll = self.get_collection(path) | ||||
|         self.service.db.delete_item(path) | ||||
|         self.service.send_signal(coll, "org.freedesktop.Secret.Collection", | ||||
|                                        "ItemDeleted", | ||||
|                                        "o", | ||||
|                                        path) | ||||
|         self.service.send_signal(coll, "org.freedesktop.DBus.Properties", | ||||
|                                        "PropertiesChanged", | ||||
|                                        "sa{sv}as", | ||||
|                                        "org.freedesktop.Secret.Collection", | ||||
|                                        {"Items": self.service.fallback_collection.get_items(coll)}, | ||||
|                                        []) | ||||
|         return NullObject | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Item", "o", "(oayays)", | ||||
|                          path_keyword="path") | ||||
|     def GetSecret(self, session, path=None): | ||||
|         session = self.service.path_objects[session] | ||||
|         sec_data, sec_type = self.service.db.get_secret(path) | ||||
|         sec_ct, sec_iv = session.encrypt(sec_data) | ||||
|         return (session.bus_path, sec_iv, sec_ct, sec_type) | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Item", "(oayays)", "", | ||||
|                          path_keyword="path") | ||||
|     def SetSecret(self, secret, path=None): | ||||
|         session, sec_param, sec_ct, sec_type = secret | ||||
|         session = self.service.path_objects[session] | ||||
|         secret = session.decrypt(sec_ct, sec_param) | ||||
|         self.service.db.set_secret(path, secret) | ||||
							
								
								
									
										147
									
								
								scripts/earthly/secretsd/secretsd/service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								scripts/earthly/secretsd/secretsd/service.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| from collections import defaultdict | ||||
| import dbus | ||||
| import dbus.lowlevel | ||||
| import dbus.service | ||||
| import time | ||||
|  | ||||
| from .collection import SecretServiceCollectionFallback | ||||
| from .exception import * | ||||
| from .item import SecretServiceItemFallback | ||||
| from .session import SecretServiceSession | ||||
| from .util import BusObjectWithProperties, NullObject | ||||
|  | ||||
| def encode_path_component(value): | ||||
|     return "".join([c if c.isalnum() else "_%02x" % ord(c) for c in value]) | ||||
|  | ||||
| class SecretService(dbus.service.Object, BusObjectWithProperties): | ||||
|     def __init__(self, bus, sdb): | ||||
|         self.bus = bus | ||||
|         self.db = sdb | ||||
|         self.bus_name = dbus.service.BusName("org.freedesktop.secrets", self.bus) | ||||
|         self.path_objects = {} | ||||
|         self.next_object = 0 | ||||
|         self.client_objects = defaultdict(list) | ||||
|  | ||||
|         super().__init__(self.bus, "/org/freedesktop/secrets") | ||||
|  | ||||
|         self.fallback_item = SecretServiceItemFallback(self) | ||||
|         self.fallback_collection = SecretServiceCollectionFallback(self) | ||||
|         self.fallback_alias = SecretServiceCollectionFallback(self, "/org/freedesktop/secrets/aliases") | ||||
|  | ||||
|     def get_collections(self, path=None): | ||||
|         collections = self.db.list_collections() | ||||
|         return dbus.Array(collections, "o") | ||||
|  | ||||
|     INTERFACE = "org.freedesktop.Secret.Service" | ||||
|     PROPERTIES = { | ||||
|         "Collections": (get_collections, None, None), | ||||
|     } | ||||
|  | ||||
|     def make_bus_path(self, persist, template): | ||||
|         if persist: | ||||
|             bus_path = template % self.db.get_next_object_id() | ||||
|         else: | ||||
|             bus_path = template % self.next_object | ||||
|             self.next_object += 1 | ||||
|         return bus_path | ||||
|  | ||||
|     def make_object(self, sender, persist, type, *args, **kwargs): | ||||
|         bus_path = self.make_bus_path(persist, type.PATH) | ||||
|         object = type(self, bus_path, *args, **kwargs) | ||||
|         self.path_objects[bus_path] = object | ||||
|         if sender: | ||||
|             self.client_objects[sender].append(bus_path) | ||||
|         return object | ||||
|  | ||||
|     def gc_client(self, sender): | ||||
|         # TODO: hook this up | ||||
|         if sender in self.client_objects: | ||||
|             for path in self.client_objects[sender]: | ||||
|                 del self.path_objects[path] | ||||
|             del self.client_objects[sender] | ||||
|  | ||||
|     def send_signal(self, path, interface, member, signature, *args): | ||||
|         msg = dbus.lowlevel.SignalMessage(path, interface, member) | ||||
|         msg.append(*args, signature=signature) | ||||
|         self.bus.send_message(msg) | ||||
|  | ||||
|     ## bus methods | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Service", "a{sv}s", "oo", | ||||
|                          sender_keyword="sender") | ||||
|     def CreateCollection(self, properties, alias, | ||||
|                          sender=None): | ||||
|         label = properties["org.freedesktop.Secret.Collection.Label"] | ||||
|         bus_path = self.make_bus_path(True, "/org/freedesktop/secrets/collection/c%d") | ||||
|         self.db.add_collection(bus_path, label) | ||||
|         if alias: | ||||
|             self.db.add_alias(alias, bus_path) | ||||
|         self.CollectionCreated(bus_path) | ||||
|         self.PropertiesChanged("org.freedesktop.Secret.Service", | ||||
|                                {"Collections": self.get_collections()}, | ||||
|                                []) | ||||
|  | ||||
|         return (dbus.ObjectPath(bus_path), NullObject) | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Service", "aoo", "a{o(oayays)}") | ||||
|     def GetSecrets(self, items, session): | ||||
|         session = self.path_objects[session] | ||||
|         out = {} | ||||
|         for item_path in items: | ||||
|             sec_data, sec_type = self.db.get_secret(item_path) | ||||
|             sec_ct, sec_iv = session.encrypt(sec_data) | ||||
|             out[item_path] = (session.bus_path, sec_iv, sec_ct, sec_type) | ||||
|         return out | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Service", "sv", "vo", | ||||
|                          sender_keyword="sender", | ||||
|                          byte_arrays=True) | ||||
|     def OpenSession(self, algorithm, input, sender=None): | ||||
|         session = self.make_object(sender, False, SecretServiceSession, algorithm) | ||||
|         output, done = session.kex(input) | ||||
|         if done: | ||||
|             return (output, session.bus_path) | ||||
|         else: | ||||
|             return (output, NullObject) | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Service", "s", "o") | ||||
|     def ReadAlias(self, alias): | ||||
|         path = self.db.resolve_alias(alias) | ||||
|         return dbus.ObjectPath(path or NullObject) | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Service", "so", "") | ||||
|     def SetAlias(self, alias, collection): | ||||
|         if alias != "default": | ||||
|             raise dbus.DBusException("Only the 'default' alias is supported", | ||||
|                                      name="org.freedesktop.DBus.Error.NotSupported") | ||||
|         if not self.db.collection_exists(collection): | ||||
|             raise dbus.DBusException("Collection with path %r not found" % (str(collection),), | ||||
|                                      name="org.freedesktop.DBus.Error.InvalidArgs") | ||||
|         self.db.add_alias(alias, collection) | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Service", "a{ss}", "aoao") | ||||
|     def SearchItems(self, attributes): | ||||
|         unlocked_items = self.db.find_items(attributes) | ||||
|         locked_items = [] | ||||
|         return unlocked_items, locked_items | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Service", "ao", "aoo") | ||||
|     def Lock(self, objects): | ||||
|         print("TODO: Service.Lock(%r)" % objects) | ||||
|         raise NotYetImplementedException() | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.Secret.Service", "ao", "aoo") | ||||
|     def Unlock(self, objects): | ||||
|         return (objects, NullObject) | ||||
|  | ||||
|     @dbus.service.signal("org.freedesktop.Secret.Service", "o") | ||||
|     def CollectionCreated(self, bus_path): | ||||
|         pass | ||||
|  | ||||
|     @dbus.service.signal("org.freedesktop.Secret.Service", "o") | ||||
|     def CollectionDeleted(self, bus_path): | ||||
|         pass | ||||
|  | ||||
|     @dbus.service.signal("org.freedesktop.Secret.Service", "o") | ||||
|     def CollectionChanged(self, bus_path): | ||||
|         pass | ||||
							
								
								
									
										55
									
								
								scripts/earthly/secretsd/secretsd/session.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								scripts/earthly/secretsd/secretsd/session.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import dbus | ||||
| import dbus.service | ||||
| import os | ||||
|  | ||||
| from .crypto_backend import ( | ||||
|     AES_BLOCK_BYTES, | ||||
|     aes_cbc_encrypt, | ||||
|     aes_cbc_decrypt, | ||||
|     dh_modp1024_exchange, | ||||
|     hkdf_sha256_derive, | ||||
|     pkcs7_pad, | ||||
|     pkcs7_unpad, | ||||
| ) | ||||
|  | ||||
| class SecretServiceSession(dbus.service.Object): | ||||
|     ROOT = "/org/freedesktop/secrets/session" | ||||
|     PATH = "/org/freedesktop/secrets/session/s%d" | ||||
|  | ||||
|     def __init__(self, service, bus_path, algorithm): | ||||
|         self.bus_path = bus_path | ||||
|         self.algorithm = algorithm | ||||
|         self.kex_done = False | ||||
|         self.crypt_key = None | ||||
|         super().__init__(service.bus, bus_path) | ||||
|  | ||||
|     def kex(self, input): | ||||
|         if self.algorithm == "plain": | ||||
|             return (dbus.ByteArray(b""), True) | ||||
|         elif self.algorithm == "dh-ietf1024-sha256-aes128-cbc-pkcs7": | ||||
|             peer_pubkey = int.from_bytes(input, "big") | ||||
|             our_pubkey, shared_key = dh_modp1024_exchange(peer_pubkey) | ||||
|             self.crypt_key = hkdf_sha256_derive(shared_key, 128 // 8) | ||||
|             output = our_pubkey.to_bytes(1024 // 8, "big") | ||||
|             return (dbus.ByteArray(output), True) | ||||
|         else: | ||||
|             raise dbus.DBusException("org.freedesktop.DBus.Error.NotSupported") | ||||
|  | ||||
|     def encrypt(self, input): | ||||
|         if self.algorithm == "plain": | ||||
|             return input, None | ||||
|         elif self.algorithm == "dh-ietf1024-sha256-aes128-cbc-pkcs7": | ||||
|             key = self.crypt_key | ||||
|             iv = os.urandom(AES_BLOCK_BYTES) | ||||
|             ct = pkcs7_pad(input, AES_BLOCK_BYTES) | ||||
|             ct = aes_cbc_encrypt(ct, key, iv) | ||||
|             return ct, iv | ||||
|  | ||||
|     def decrypt(self, input, iv): | ||||
|         if self.algorithm == "plain": | ||||
|             return input | ||||
|         elif self.algorithm == "dh-ietf1024-sha256-aes128-cbc-pkcs7": | ||||
|             key = self.crypt_key | ||||
|             pt = aes_cbc_decrypt(input, key, iv) | ||||
|             pt = pkcs7_unpad(pt, AES_BLOCK_BYTES) | ||||
|             return pt | ||||
							
								
								
									
										55
									
								
								scripts/earthly/secretsd/secretsd/util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								scripts/earthly/secretsd/secretsd/util.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import dbus | ||||
| import dbus.service | ||||
|  | ||||
| from .exception import InvalidArgsException | ||||
|  | ||||
| NullObject = dbus.ObjectPath("/") | ||||
|  | ||||
| class BusObjectWithProperties(): | ||||
|     PROPERTIES = {} | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.DBus.Properties", "ss", "v", | ||||
|                          path_keyword="path") | ||||
|     def Get(self, interface, property, path=None): | ||||
|         if interface == self.INTERFACE: | ||||
|             if property in self.PROPERTIES: | ||||
|                 getter, setter, value = self.PROPERTIES[property] | ||||
|                 if getter: | ||||
|                     value = getter(self, path) | ||||
|                 return value | ||||
|             else: | ||||
|                 raise InvalidArgsException("No such property %r" % str(property)) | ||||
|         else: | ||||
|             raise InvalidArgsException("No such interface %r" % str(interface)) | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.DBus.Properties", "s", "a{sv}", | ||||
|                          path_keyword="path") | ||||
|     def GetAll(self, interface, path=None): | ||||
|         if interface == self.INTERFACE: | ||||
|             out = {} | ||||
|             for name, (getter, setter, value) in self.PROPERTIES.items(): | ||||
|                 if getter: | ||||
|                     value = getter(self, path) | ||||
|                 out[name] = value | ||||
|             return out | ||||
|         else: | ||||
|             raise InvalidArgsException("No such interface %r" % str(interface)) | ||||
|  | ||||
|     @dbus.service.method("org.freedesktop.DBus.Properties", "ssv", "", | ||||
|                          path_keyword="path") | ||||
|     def Set(self, interface, property, value, path=None): | ||||
|         if interface == self.INTERFACE: | ||||
|             if property in self.PROPERTIES: | ||||
|                 getter, setter, _ = self.PROPERTIES[property] | ||||
|                 if setter: | ||||
|                     setter(self, path, value) | ||||
|                 else: | ||||
|                     raise InvalidArgsException("Property %r is read-only" % str(property)) | ||||
|             else: | ||||
|                 raise InvalidArgsException("No such property %r" % str(property)) | ||||
|         else: | ||||
|             raise InvalidArgsException("No such interface %r" % str(interface)) | ||||
|  | ||||
|     @dbus.service.signal("org.freedesktop.DBus.Properties", "sa{sv}as") | ||||
|     def PropertiesChanged(self, interface, changed_props, invalidated_props): | ||||
|         pass | ||||
		Reference in New Issue
	
	Block a user