xi-keyring

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

commit
0c69d0cf2ecf58fe7de8fe68ac87452b03e8ba81
parent
6cf5c614e5be2d8939f13486f0d8cc7e5d8bb33c
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2026-05-24 10:53
read store from caller's mount namespace

Diffstat

M xikeyring/keyring.py 18 ++++++++++++++----
M xikeyring/pidfd.py 24 ++++++++++++++++++++++++

2 files changed, 38 insertions, 4 deletions


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

@@ -27,9 +27,11 @@ class Item:
   27    27     attributes: dict[str, str]
   28    28 
   29    29 
   30    -1 def write_bytes(path: Path, data: bytes) -> int:
   -1    30 def write_bytes(path: Path, data: bytes, pid: PID | None = None) -> int:
   31    31     flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
   32    32     fd = os.open(path, flags, mode=0o600)
   -1    33     if pid:
   -1    34         pid.check_active()
   33    35     try:
   34    36         return os.write(fd, data)
   35    37     finally:
@@ -88,10 +90,12 @@ class Keyring:
   88    90         return KernelKey(key)
   89    91 
   90    92     def _read(self, pid: PID) -> dict[int, Item]:
   91    -1         if not self.path.exists():
   -1    93         path = pid.path(self.path)
   -1    94         if not path.exists():
   92    95             return {}
   93    96 
   94    -1         encrypted = self.path.read_bytes()
   -1    97         encrypted = path.read_bytes()
   -1    98         pid.check_active()
   95    99         decrypted = Fernet(self.key.value).decrypt(encrypted)
   96   100         raw = json.loads(decrypted)
   97   101         return {
@@ -100,6 +104,12 @@ class Keyring:
  100   104         }
  101   105 
  102   106     def _write(self, pid: PID, items: dict[int, Item]):
   -1   107         path = pid.path(self.path)
   -1   108         if not path.parent.exists():
   -1   109             # Raise an error instead of creating the directory because this
   -1   110             # might be a tmpfs.
   -1   111             raise NotFoundError
   -1   112 
  103   113         raw = [
  104   114             (
  105   115                 id,
@@ -110,7 +120,7 @@ class Keyring:
  110   120         ]
  111   121         decrypted = json.dumps(raw).encode('utf-8')
  112   122         encrypted = Fernet(self.key.value).encrypt(decrypted)
  113    -1         write_bytes(self.path, encrypted)
   -1   123         write_bytes(path, encrypted, pid)
  114   124 
  115   125     def confirm_access(self) -> None:
  116   126         if not self.prompt.confirm('Allow access to a secret from your keyring?'):

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

@@ -1,4 +1,28 @@
   -1     1 import selectors
   -1     2 from pathlib import Path
   -1     3 
   -1     4 
    1     5 class PID:
    2     6     def __init__(self, pid: int, pidfd: int):
    3     7         self.pid = pid
    4     8         self.pidfd = pidfd
   -1     9 
   -1    10     def check_active(self) -> None:
   -1    11         with selectors.DefaultSelector() as sel:
   -1    12             sel.register(self.pidfd, selectors.EVENT_READ)
   -1    13             if sel.select(0) != []:
   -1    14                 raise ValueError('Calling process has quit')
   -1    15 
   -1    16     def path(self, path: str | Path) -> Path:
   -1    17         root = (Path('/proc') / str(self.pid) / 'root').resolve()
   -1    18         rel_path = Path(path).absolute().relative_to('/')
   -1    19         result = (root / rel_path).resolve()
   -1    20 
   -1    21         # FIXME: symlinks are resoled relative to the host.
   -1    22         #
   -1    23         # A proper fix would involve openat2()
   -1    24         # (see https://github.com/python/cpython/issues/141878).
   -1    25         if root not in result.parents:
   -1    26             raise ValueError('path escapes mount namespace')
   -1    27 
   -1    28         return result