xi-keyring

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

commit
6cf5c614e5be2d8939f13486f0d8cc7e5d8bb33c
parent
da6b828060bd429f95bfeab8ccbe8b708f9f51df
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2026-05-24 10:59
pass pid instead of app_id

Diffstat

D xikeyring/app_id.py 21 ---------------------
M xikeyring/dbus.py 61 +++++++++++++++++++++++++++++++------------------------------
M xikeyring/keyring.py 47 ++++++++++++++++++++++++-----------------------
A xikeyring/pidfd.py 4 ++++

4 files changed, 59 insertions, 74 deletions


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

@@ -1,21 +0,0 @@
    1    -1 import configparser
    2    -1 import selectors
    3    -1 from pathlib import Path
    4    -1 
    5    -1 
    6    -1 def get_app_id(pid: int, pidfd: int) -> str:
    7    -1     path = Path('/proc') / str(pid) / 'root' / '.flatpak-info'
    8    -1     config = configparser.ConfigParser()
    9    -1     try:
   10    -1         with path.open() as fh:
   11    -1             config.read_file(fh)
   12    -1         app_id = config['Application']['name']
   13    -1     except Exception:
   14    -1         return ''
   15    -1 
   16    -1     with selectors.DefaultSelector() as sel:
   17    -1         sel.register(pidfd, selectors.EVENT_READ)
   18    -1         if sel.select(0) != []:
   19    -1             raise ValueError('Calling process has quit')
   20    -1 
   21    -1     return app_id

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

@@ -8,10 +8,10 @@ import gi
    8     8 from gi.repository import Gio
    9     9 from gi.repository import GLib
   10    10 
   11    -1 from .app_id import get_app_id
   12    11 from .dbus_sessions import create_session
   13    12 from .keyring import AccessDeniedError
   14    13 from .keyring import NotFoundError
   -1    14 from .pidfd import PID
   15    15 
   16    16 OFSP = '/org/freedesktop/secrets'
   17    17 OFSI = 'org.freedesktop.Secret'
@@ -115,7 +115,7 @@ class BaseDBusService:
  115   115         self._call(conn, sender, path, iface, f'Set{prop}', [value], error)
  116   116         return True
  117   117 
  118    -1     def get_app_id(self, conn, sender) -> str:
   -1   118     def get_pid(self, conn, sender) -> PID:
  119   119         (cred,), fds = conn.call_with_unix_fd_list_sync(
  120   120             'org.freedesktop.DBus',
  121   121             '/org/freedesktop/DBus',
@@ -128,7 +128,7 @@ class BaseDBusService:
  128   128             Gio.UnixFDList(),
  129   129             None,
  130   130         )
  131    -1         return get_app_id(cred['ProcessID'], fds.get(cred['ProcessFD']))
   -1   131         return PID(cred['ProcessID'], fds.get(cred['ProcessFD']))
  132   132 
  133   133 
  134   134 class DBusService(BaseDBusService):
@@ -185,8 +185,8 @@ class DBusService(BaseDBusService):
  185   185                 ),
  186   186             )
  187   187 
  188    -1     def search_items(self, app_id, conn, query={}):
  189    -1         items = self.keyring.search_items(app_id, query)
   -1   188     def search_items(self, pid, conn, query={}):
   -1   189         items = self.keyring.search_items(pid, query)
  190   190         self.update_items(conn, add=items, emit=False)
  191   191         return items
  192   192 
@@ -231,8 +231,8 @@ class DBusService(BaseDBusService):
  231   231                 self.session_close(conn, sender, path)
  232   232 
  233   233     def service_search_items(self, conn, sender, path, query):
  234    -1         app_id = self.get_app_id(conn, sender)
  235    -1         items = self.search_items(app_id, conn, query)
   -1   234         pid = self.get_pid(conn, sender)
   -1   235         items = self.search_items(pid, conn, query)
  236   236         return GLib.Variant('(aoao)', (self.ids_to_paths(items), []))
  237   237 
  238   238     def service_unlock(self, conn, sender, path, objects):
@@ -244,11 +244,11 @@ class DBusService(BaseDBusService):
  244   244 
  245   245     def service_get_secrets(self, conn, sender, path, items, session_path):
  246   246         session = self.sessions[session_path][2]
  247    -1         app_id = self.get_app_id(conn, sender)
   -1   247         pid = self.get_pid(conn, sender)
  248   248         result = []
  249   249         for path in items:
  250   250             id = int(path.rsplit('/', 1)[1], 10)
  251    -1             secret = self.keyring.get_secret(app_id, id)
   -1   251             secret = self.keyring.get_secret(pid, id)
  252   252             secret_tuple = session.encode(session_path, secret)
  253   253             result.append((path, secret_tuple))
  254   254         return GLib.Variant('(a{o(oayays)})', [result])
@@ -263,8 +263,8 @@ class DBusService(BaseDBusService):
  263   263         return GLib.Variant('ao', [f'{OFSP}/collection/it'])
  264   264 
  265   265     def collection_search_items(self, conn, sender, path, query):
  266    -1         app_id = self.get_app_id(conn, sender)
  267    -1         items = self.search_items(app_id, conn, query)
   -1   266         pid = self.get_pid(conn, sender)
   -1   267         items = self.search_items(pid, conn, query)
  268   268         return GLib.Variant('(ao)', [self.ids_to_paths(items)])
  269   269 
  270   270     def collection_create_item(
@@ -273,21 +273,21 @@ class DBusService(BaseDBusService):
  273   273         session = self.sessions[secret_tuple[0]][2]
  274   274         secret = session.decode(secret_tuple)
  275   275         attributes = properties.get(f'{OFSI}.Item.Attributes', {})
  276    -1         app_id = self.get_app_id(conn, sender)
   -1   276         pid = self.get_pid(conn, sender)
  277   277         id = None
  278   278         if replace:
  279    -1             matches = self.search_items(app_id, conn, attributes)
   -1   279             matches = self.search_items(pid, conn, attributes)
  280   280             if matches:
  281   281                 id = matches[0]
  282    -1                 self.keyring.update_secret(app_id, id, secret)
   -1   282                 self.keyring.update_secret(pid, id, secret)
  283   283         if not id:
  284    -1             id = self.keyring.create_item(app_id, attributes, secret)
   -1   284             id = self.keyring.create_item(pid, attributes, secret)
  285   285             self.update_items(conn, add=[id])
  286   286         return GLib.Variant('(oo)', (f'{OFSP}/collection/it/{id}', '/'))
  287   287 
  288   288     def collection_get_items(self, conn, sender, path):
  289    -1         app_id = self.get_app_id(conn, sender)
  290    -1         items = self.search_items(app_id, conn)
   -1   289         pid = self.get_pid(conn, sender)
   -1   290         items = self.search_items(pid, conn)
  291   291         return GLib.Variant('ao', self.ids_to_paths(items))
  292   292 
  293   293     def collection_get_label(self, conn, sender, path):
@@ -304,15 +304,15 @@ class DBusService(BaseDBusService):
  304   304 
  305   305     def item_delete(self, conn, sender, path):
  306   306         id = int(path.rsplit('/', 1)[1], 10)
  307    -1         app_id = self.get_app_id(conn, sender)
  308    -1         self.keyring.delete_item(app_id, id)
   -1   307         pid = self.get_pid(conn, sender)
   -1   308         self.keyring.delete_item(pid, id)
  309   309         self.update_items(conn, rm=[id])
  310   310         return GLib.Variant('(o)', ['/'])
  311   311 
  312   312     def item_get_secret(self, conn, sender, path, session_path):
  313   313         id = int(path.rsplit('/', 1)[1], 10)
  314    -1         app_id = self.get_app_id(conn, sender)
  315    -1         secret = self.keyring.get_secret(app_id, id)
   -1   314         pid = self.get_pid(conn, sender)
   -1   315         secret = self.keyring.get_secret(pid, id)
  316   316         session = self.sessions[session_path][2]
  317   317         secret_tuple = session.encode(session_path, secret)
  318   318         return GLib.Variant('((oayays))', [secret_tuple])
@@ -321,8 +321,8 @@ class DBusService(BaseDBusService):
  321   321         id = int(path.rsplit('/', 1)[1], 10)
  322   322         session = self.sessions[secret_tuple[0]][2]
  323   323         secret = session.decode(secret_tuple)
  324    -1         app_id = self.get_app_id(conn, sender)
  325    -1         self.keyring.update_secret(app_id, id, secret)
   -1   324         pid = self.get_pid(conn, sender)
   -1   325         self.keyring.update_secret(pid, id, secret)
  326   326 
  327   327     def item_get_label(self, conn, sender, path):
  328   328         return GLib.Variant('s', path.rsplit('/', 1)[1])
@@ -341,15 +341,15 @@ class DBusService(BaseDBusService):
  341   341 
  342   342     def item_get_attributes(self, conn, sender, path):
  343   343         id = int(path.rsplit('/', 1)[1], 10)
  344    -1         app_id = self.get_app_id(conn, sender)
  345    -1         attributes = self.keyring.get_attributes(app_id, id)
   -1   344         pid = self.get_pid(conn, sender)
   -1   345         attributes = self.keyring.get_attributes(pid, id)
  346   346         return GLib.Variant('a{ss}', attributes.items())
  347   347 
  348   348     def item_set_attributes(self, conn, sender, path, value):
  349   349         id = int(path.rsplit('/', 1)[1], 10)
  350   350         attributes = value.unpack()
  351    -1         app_id = self.get_app_id(conn, sender)
  352    -1         self.keyring.update_attributes(app_id, id, attributes)
   -1   351         pid = self.get_pid(conn, sender)
   -1   352         self.keyring.update_attributes(pid, id, attributes)
  353   353 
  354   354         conn.emit_signal(
  355   355             None,
@@ -380,16 +380,17 @@ class DBusService(BaseDBusService):
  380   380             conn, handle, 'org.freedesktop.impl.portal.Request'
  381   381         )
  382   382         try:
   -1   383             pid = self.get_pid(conn, sender)
  383   384             attrs = {
  384   385                 'application': 'org.freedesktop.portal.Secret',
  385   386                 'app_id': app_id,
  386   387             }
  387    -1             ids = self.keyring.search_items('', attrs)
   -1   388             ids = self.keyring.search_items(pid, attrs)
  388   389             if ids:
  389    -1                 secret = self.keyring.get_secret('', ids[0])
   -1   390                 secret = self.keyring.get_secret(pid, ids[0])
  390   391             else:
  391   392                 secret = os.urandom(64)
  392    -1                 self.keyring.create_item('', attrs, secret)
   -1   393                 self.keyring.create_item(pid, attrs, secret)
  393   394             os.write(fd, secret)
  394   395             os.close(fd)
  395   396         finally:

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

@@ -9,6 +9,7 @@ from cryptography.fernet import InvalidToken
    9     9 
   10    10 from . import crypto
   11    11 from .kernel_keyring import KernelKey
   -1    12 from .pidfd import PID
   12    13 from .prompt import PinentryPrompt as Prompt
   13    14 
   14    15 
@@ -86,7 +87,7 @@ class Keyring:
   86    87         write_bytes(path, encrypted)
   87    88         return KernelKey(key)
   88    89 
   89    -1     def _read(self) -> dict[int, Item]:
   -1    90     def _read(self, pid: PID) -> dict[int, Item]:
   90    91         if not self.path.exists():
   91    92             return {}
   92    93 
@@ -98,7 +99,7 @@ class Keyring:
   98    99             for id, secret, attributes in raw
   99   100         }
  100   101 
  101    -1     def _write(self, items: dict[int, Item]):
   -1   102     def _write(self, pid: PID, items: dict[int, Item]):
  102   103         raw = [
  103   104             (
  104   105                 id,
@@ -125,50 +126,50 @@ class Keyring:
  125   126         except KeyError as e:
  126   127             raise NotFoundError from e
  127   128 
  128    -1     def search_items(self, app_id: str, query: dict[str, str] = {}) -> list[int]:
  129    -1         items = self._read()
   -1   129     def search_items(self, pid: PID, query: dict[str, str] = {}) -> list[int]:
   -1   130         items = self._read(pid)
  130   131         return [
  131   132             id for id, item in items.items()
  132   133             if all(item.attributes.get(k) == v for k, v in query.items())
  133   134         ]
  134   135 
  135    -1     def get_attributes(self, app_id: str, id: int) -> dict[str, str]:
  136    -1         items = self._read()
  137    -1         return self.get(items, app_id, id).attributes
   -1   136     def get_attributes(self, pid: PID, id: int) -> dict[str, str]:
   -1   137         items = self._read(pid)
   -1   138         return self.get(items, id).attributes
  138   139 
  139    -1     def get_secret(self, app_id: str, id: int) -> bytes:
  140    -1         items = self._read()
   -1   140     def get_secret(self, pid: PID, id: int) -> bytes:
   -1   141         items = self._read(pid)
  141   142         item = self.get(items, id)
  142   143         self.confirm_access()
  143   144         return item.secret
  144   145 
  145    -1     def create_item(self, app_id: str, attributes: dict[str, str], secret: bytes) -> int:
  146    -1         items = self._read()
   -1   146     def create_item(self, pid: PID, attributes: dict[str, str], secret: bytes) -> int:
   -1   147         items = self._read(pid)
  147   148         id = max(items.keys(), default=0) + 1
  148    -1         items[id] = Item(secret, attributes, app_id)
  149    -1         self._write(items)
   -1   149         items[id] = Item(secret, attributes)
   -1   150         self._write(pid, items)
  150   151         return id
  151   152 
  152    -1     def update_attributes(self, app_id: str, id: int, attributes: dict[str, str]) -> None:
  153    -1         items = self._read()
   -1   153     def update_attributes(self, pid: PID, id: int, attributes: dict[str, str]) -> None:
   -1   154         items = self._read(pid)
  154   155         item = self.get(items, id)
  155   156         self.confirm_change()
  156   157         item.attributes = attributes
  157    -1         self._write(items)
   -1   158         self._write(pid, items)
  158   159 
  159    -1     def update_secret(self, app_id: str, id: int, secret: bytes) -> None:
  160    -1         items = self._read()
   -1   160     def update_secret(self, pid: PID, id: int, secret: bytes) -> None:
   -1   161         items = self._read(pid)
  161   162         item = self.get(items, id)
  162   163         self.confirm_change()
  163   164         item.secret = secret
  164    -1         self._write(items)
   -1   165         self._write(pid, items)
  165   166 
  166    -1     def delete_item(self, app_id: str, id: int) -> None:
  167    -1         items = self._read()
  168    -1         self.get(items, app_id, id)  # trigger appropriate exceptions
   -1   167     def delete_item(self, pid: PID, id: int) -> None:
   -1   168         items = self._read(pid)
   -1   169         self.get(items, id)  # trigger appropriate exceptions
  169   170         self.confirm_change()
  170   171         del items[id]
  171    -1         self._write(items)
   -1   172         self._write(pid, items)
  172   173 
  173   174 
  174   175 class KeyringProxy:

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

@@ -0,0 +1,4 @@
   -1     1 class PID:
   -1     2     def __init__(self, pid: int, pidfd: int):
   -1     3         self.pid = pid
   -1     4         self.pidfd = pidfd