xi-keyring

simple and extensible alternative for gnome-keyring
git clone https://git.ce9e.org/xi-keyring.git

commit
c6af7f9f0a1cbadccb2ce7f70c509304ece03a45
parent
58259b8b0382a52b548bf1799564c1b058eb97b2
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2024-04-05 06:18
restrict access based on sender exe

Diffstat

M xikeyring/dbus.py 53 +++++++++++++++++++++++++++++++++++++++--------------
M xikeyring/keyring.py 71 ++++++++++++++++++++++++++++++++++++-------------------------

2 files changed, 81 insertions, 43 deletions


diff --git a/xikeyring/dbus.py b/xikeyring/dbus.py

@@ -1,4 +1,5 @@
    1     1 import logging
   -1     2 import os
    2     3 import re
    3     4 import sys
    4     5 from pathlib import Path
@@ -104,6 +105,20 @@ class BaseDBusService:
  104   105         self._call(conn, sender, path, iface, f'Set{prop}', [value], error)
  105   106         return True
  106   107 
   -1   108     def get_exe(self, conn, sender) -> str:
   -1   109         pid = conn.call_sync(
   -1   110             'org.freedesktop.DBus',
   -1   111             '/org/freedesktop/DBus',
   -1   112             'org.freedesktop.DBus',
   -1   113             'GetConnectionUnixProcessID',
   -1   114             GLib.Variant('(s)', [sender]),
   -1   115             GLib.VariantType('(u)'),
   -1   116             Gio.DBusCallFlags.NONE,
   -1   117             -1,
   -1   118             None,
   -1   119         )[0]
   -1   120         return os.readlink(f'/proc/{pid}/exe')
   -1   121 
  107   122 
  108   123 class DBusService(BaseDBusService):
  109   124     def __init__(self, keyring):
@@ -163,8 +178,8 @@ class DBusService(BaseDBusService):
  163   178                     ),
  164   179                 )
  165   180 
  166    -1     def search_items(self, conn, query={}, *, emit=True):
  167    -1         items = self.keyring.search_items(query)
   -1   181     def search_items(self, exe, conn, query={}, *, emit=True):
   -1   182         items = self.keyring.search_items(exe, query)
  168   183         if query:
  169   184             self.update_items(conn, add=items, emit=emit)
  170   185         else:
@@ -186,7 +201,8 @@ class DBusService(BaseDBusService):
  186   201         return GLib.Variant('(vo)', (GLib.Variant('ay', output), session_path))
  187   202 
  188   203     def service_search_items(self, conn, sender, path, query):
  189    -1         items = self.search_items(conn, query)
   -1   204         exe = self.get_exe(conn, sender)
   -1   205         items = self.search_items(exe, conn, query)
  190   206         return GLib.Variant('(aoao)', (self.ids_to_paths(items), []))
  191   207 
  192   208     def service_unlock(self, conn, sender, path, objects):
@@ -198,10 +214,11 @@ class DBusService(BaseDBusService):
  198   214 
  199   215     def service_get_secrets(self, conn, sender, path, items, session_path):
  200   216         session = self.sessions[session_path]
   -1   217         exe = self.get_exe(conn, sender)
  201   218         result = []
  202   219         for path in items:
  203   220             id = int(path.rsplit('/', 1)[1], 10)
  204    -1             secret = self.keyring.get_secret(id)
   -1   221             secret = self.keyring.get_secret(exe, id)
  205   222             secret_tuple = session.encode(session_path, secret)
  206   223             result.append((path, secret_tuple))
  207   224         return GLib.Variant('(a{o(oayays)})', [result])
@@ -216,7 +233,8 @@ class DBusService(BaseDBusService):
  216   233         return GLib.Variant('ao', [f'{OFSP}/collection/it'])
  217   234 
  218   235     def collection_search_items(self, conn, sender, path, query):
  219    -1         items = self.search_items(conn, query)
   -1   236         exe = self.get_exe(conn, sender)
   -1   237         items = self.search_items(exe, conn, query)
  220   238         return GLib.Variant('(ao)', [self.ids_to_paths(items)])
  221   239 
  222   240     def collection_create_item(
@@ -225,19 +243,21 @@ class DBusService(BaseDBusService):
  225   243         session = self.sessions[secret_tuple[0]]
  226   244         secret = session.decode(secret_tuple)
  227   245         attributes = properties.get(f'{OFSI}.Item.Attributes', {})
   -1   246         exe = self.get_exe(conn, sender)
  228   247         id = None
  229   248         if replace:
  230    -1             matches = self.search_items(conn, attributes)
   -1   249             matches = self.search_items(exe, conn, attributes)
  231   250             if matches:
  232   251                 id = matches[0]
  233    -1                 self.keyring.update_secret(id, secret)
   -1   252                 self.keyring.update_secret(exe, id, secret)
  234   253         if not id:
  235    -1             id = self.keyring.create_item(attributes, secret)
   -1   254             id = self.keyring.create_item(exe, attributes, secret)
  236   255             self.update_items(conn, add=[id])
  237   256         return GLib.Variant('(oo)', (f'{OFSP}/collection/it/{id}', '/'))
  238   257 
  239   258     def collection_get_items(self, conn, sender, path):
  240    -1         items = self.search_items(conn)
   -1   259         exe = self.get_exe(conn, sender)
   -1   260         items = self.search_items(exe, conn)
  241   261         return GLib.Variant('ao', self.ids_to_paths(items))
  242   262 
  243   263     def collection_get_label(self, conn, sender, path):
@@ -254,13 +274,15 @@ class DBusService(BaseDBusService):
  254   274 
  255   275     def item_delete(self, conn, sender, path):
  256   276         id = int(path.rsplit('/', 1)[1], 10)
  257    -1         self.keyring.delete_item(id)
   -1   277         exe = self.get_exe(conn, sender)
   -1   278         self.keyring.delete_item(exe, id)
  258   279         self.update_items(conn, rm=[id])
  259   280         return GLib.Variant('(o)', ['/'])
  260   281 
  261   282     def item_get_secret(self, conn, sender, path, session_path):
  262   283         id = int(path.rsplit('/', 1)[1], 10)
  263    -1         secret = self.keyring.get_secret(id)
   -1   284         exe = self.get_exe(conn, sender)
   -1   285         secret = self.keyring.get_secret(exe, id)
  264   286         session = self.sessions[session_path]
  265   287         secret_tuple = session.encode(session_path, secret)
  266   288         return GLib.Variant('((oayays))', [secret_tuple])
@@ -269,7 +291,8 @@ class DBusService(BaseDBusService):
  269   291         id = int(path.rsplit('/', 1)[1], 10)
  270   292         session = self.sessions[secret_tuple[0]]
  271   293         secret = session.decode(secret_tuple)
  272    -1         self.keyring.update_secret(id, secret)
   -1   294         exe = self.get_exe(conn, sender)
   -1   295         self.keyring.update_secret(exe, id, secret)
  273   296 
  274   297     def item_get_label(self, conn, sender, path):
  275   298         return GLib.Variant('s', path.rsplit('/', 1)[1])
@@ -288,13 +311,15 @@ class DBusService(BaseDBusService):
  288   311 
  289   312     def item_get_attributes(self, conn, sender, path):
  290   313         id = int(path.rsplit('/', 1)[1], 10)
  291    -1         attributes = self.keyring.get_attributes(id)
   -1   314         exe = self.get_exe(conn, sender)
   -1   315         attributes = self.keyring.get_attributes(exe, id)
  292   316         return GLib.Variant('a{ss}', attributes.items())
  293   317 
  294   318     def item_set_attributes(self, conn, sender, path, value):
  295   319         id = int(path.rsplit('/', 1)[1], 10)
  296   320         attributes = value.unpack()
  297    -1         self.keyring.update_attributes(id, attributes)
   -1   321         exe = self.get_exe(conn, sender)
   -1   322         self.keyring.update_attributes(exe, id, attributes)
  298   323 
  299   324         conn.emit_signal(
  300   325             None,

diff --git a/xikeyring/keyring.py b/xikeyring/keyring.py

@@ -10,6 +10,10 @@ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
   10    10 
   11    11 from .prompt import PinentryPrompt as Prompt
   12    12 
   -1    13 TRUSTED_MANAGERS = [
   -1    14     '/usr/bin/seahorse',
   -1    15 ]
   -1    16 
   13    17 
   14    18 class AccessDeniedError(Exception):
   15    19     pass
@@ -23,6 +27,7 @@ class NotFoundError(Exception):
   23    27 class Item:
   24    28     secret: bytes
   25    29     attributes: dict[str, str]
   -1    30     exe: str
   26    31 
   27    32 
   28    33 class Crypt:
@@ -108,8 +113,8 @@ class Keyring:
  108   113         decrypted = self.crypt.decrypt(encrypted)
  109   114         raw = json.loads(decrypted)
  110   115         self.items = {
  111    -1             id: Item(base64.urlsafe_b64decode(secret), attributes)
  112    -1             for id, secret, attributes in raw
   -1   116             id: Item(base64.urlsafe_b64decode(secret), attributes, exe)
   -1   117             for id, secret, attributes, exe in raw
  113   118         }
  114   119 
  115   120     def _write(self):
@@ -118,6 +123,7 @@ class Keyring:
  118   123                 id,
  119   124                 base64.urlsafe_b64encode(item.secret).decode(),
  120   125                 item.attributes,
   -1   126                 item.exe,
  121   127             )
  122   128             for id, item in self.items.items()
  123   129         ]
@@ -126,57 +132,64 @@ class Keyring:
  126   132         with open(self.path, 'wb') as fh:
  127   133             fh.write(encrypted)
  128   134 
  129    -1     def confirm_access(self) -> None:
  130    -1         if not self.prompt.confirm('Allow access to secret from keyring?'):
   -1   135     def confirm_access(self, exe: str) -> None:
   -1   136         if not self.prompt.confirm(f'Allow {exe} to access a secret from yout keyring?'):
  131   137             raise AccessDeniedError
  132   138 
  133    -1     def confirm_change(self) -> None:
  134    -1         if not self.prompt.confirm('Allow changes to keyring?'):
   -1   139     def confirm_change(self, exe: str) -> None:
   -1   140         if not self.prompt.confirm(f'Allow {exe} to make changes to your keyring?'):
  135   141             raise AccessDeniedError
  136   142 
  137    -1     def __getitem__(self, id: int) -> Item:
   -1   143     def has_access(self, exe: str, item: Item) -> bool:
   -1   144         return item.exe == exe or exe in TRUSTED_MANAGERS
   -1   145 
   -1   146     def get(self, exe: str, id: int) -> Item:
  138   147         try:
  139    -1             return self.items[id]
   -1   148             item = self.items[id]
  140   149         except KeyError as e:
  141   150             raise NotFoundError from e
   -1   151         if not self.has_access(exe, item):
   -1   152             raise NotFoundError
   -1   153         return item
  142   154 
  143    -1     def search_items(self, query: dict[str, str] = {}) -> list[int]:
   -1   155     def search_items(self, exe: str, query: dict[str, str] = {}) -> list[int]:
  144   156         return [
  145   157             id for id, item in self.items.items()
  146    -1             if not query or all(
   -1   158             if self.has_access(exe, item) and all(
  147   159                 item.attributes.get(key) == value for key, value in query.items()
  148   160             )
  149   161         ]
  150   162 
  151    -1     def get_attributes(self, id: int) -> dict[str, str]:
  152    -1         return self[id].attributes
   -1   163     def get_attributes(self, exe: str, id: int) -> dict[str, str]:
   -1   164         return self.get(exe, id).attributes
  153   165 
  154    -1     def get_secret(self, id: int) -> bytes:
  155    -1         self.confirm_access()
  156    -1         return self[id].secret
   -1   166     def get_secret(self, exe: str, id: int) -> bytes:
   -1   167         item = self.get(exe, id)
   -1   168         self.confirm_access(exe)
   -1   169         return item.secret
  157   170 
  158    -1     def create_item(self, attributes: dict[str, str], secret: bytes) -> int:
   -1   171     def create_item(self, exe: str, attributes: dict[str, str], secret: bytes) -> int:
  159   172         id = max(self.items.keys(), default=0) + 1
  160    -1         self.items[id] = Item(secret, attributes)
   -1   173         self.items[id] = Item(secret, attributes, exe)
  161   174         self._write()
  162   175         return id
  163   176 
  164    -1     def update_attributes(self, id: int, attributes: dict[str, str]) -> None:
  165    -1         self.confirm_change()
  166    -1         self[id].attributes = attributes
   -1   177     def update_attributes(self, exe: str, id: int, attributes: dict[str, str]) -> None:
   -1   178         item = self.get(exe, id)
   -1   179         self.confirm_change(exe)
   -1   180         item.attributes = attributes
  167   181         self._write()
  168   182 
  169    -1     def update_secret(self, id: int, secret: bytes) -> None:
  170    -1         self.confirm_change()
  171    -1         self[id].secret = secret
   -1   183     def update_secret(self, exe: str, id: int, secret: bytes) -> None:
   -1   184         item = self.get(exe, id)
   -1   185         self.confirm_change(exe)
   -1   186         item.secret = secret
  172   187         self._write()
  173   188 
  174    -1     def delete_item(self, id: int) -> None:
  175    -1         self.confirm_change()
  176    -1         try:
  177    -1             del self.items[id]
  178    -1         except KeyError as e:
  179    -1             raise NotFoundError from e
   -1   189     def delete_item(self, exe: str, id: int) -> None:
   -1   190         self.get(exe, id)  # trigger appropriate exceptions
   -1   191         self.confirm_change(exe)
   -1   192         del self.items[id]
  180   193         self._write()
  181   194 
  182   195