xi-keyring

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

commit
88faaf1ead8d2e57d79e54de612bd494848872f6
parent
e73e6be734b40354774318e204c6e6c616c7b3ab
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2026-05-24 08:04
refactor: rm crypt

Diffstat

M xikeyring/__main__.py 5 +++--
A xikeyring/crypto.py 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
M xikeyring/keyring.py 63 +++++++------------------------------------------------------

3 files changed, 60 insertions, 58 deletions


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

@@ -3,6 +3,7 @@ import os
    3     3 import sys
    4     4 from pathlib import Path
    5     5 
   -1     6 from . import crypto
    6     7 from .dbus import DBusService
    7     8 from .dumpable import pr_set
    8     9 from .keyring import KeyringProxy
@@ -48,11 +49,11 @@ args = parse_args()
   48    49 keyring = KeyringProxy(args.store)
   49    50 if args.dump:
   50    51     encrypted = keyring.path.read_bytes()
   51    -1     decrypted = keyring.crypt.decrypt(encrypted)
   -1    52     decrypted = crypto.decrypt_with_password(encrypted, keyring.password.value)
   52    53     print(decrypted.decode('utf-8'))
   53    54 elif args.restore:
   54    55     decrypted = sys.stdin.read().encode('utf-8')
   55    -1     encrypted = keyring.crypt.encrypt(decrypted)
   -1    56     encrypted = crypto.encrypt_with_password(decrypted, keyring.password.value)
   56    57     write_bytes(keyring.path, encrypted)
   57    58 else:
   58    59     service = DBusService(keyring)

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

@@ -0,0 +1,50 @@
   -1     1 import base64
   -1     2 import os
   -1     3 
   -1     4 import argon2
   -1     5 from cryptography.fernet import Fernet
   -1     6 
   -1     7 
   -1     8 def get_argon2(
   -1     9     password: bytes,
   -1    10     salt: bytes,
   -1    11     time_cost: int,
   -1    12     memory_cost: int,
   -1    13     parallelism: int,
   -1    14 ) -> bytes:
   -1    15     # https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice
   -1    16     key = argon2.low_level.hash_secret_raw(
   -1    17         secret=password,
   -1    18         salt=salt,
   -1    19         time_cost=time_cost,
   -1    20         memory_cost=memory_cost,
   -1    21         parallelism=parallelism,
   -1    22         hash_len=32,
   -1    23         type=argon2.low_level.Type.ID,
   -1    24     )
   -1    25     return base64.urlsafe_b64encode(key)
   -1    26 
   -1    27 
   -1    28 def encrypt_with_password(data: bytes, password: bytes) -> bytes:
   -1    29     salt = os.urandom(16)
   -1    30     params = [3, 1 << 16, 4]
   -1    31     key = get_argon2(password, salt, *params)
   -1    32     content = Fernet(key).encrypt(data)
   -1    33     return b'$'.join(
   -1    34         [
   -1    35             b'fernet-argon2',
   -1    36             base64.urlsafe_b64encode(salt),
   -1    37             *[str(p).encode() for p in params],
   -1    38             content,
   -1    39         ]
   -1    40     )
   -1    41 
   -1    42 def decrypt_with_password(data: bytes, password: bytes) -> bytes:
   -1    43     algo, salt, *params, content = data.split(b'$')
   -1    44     salt = base64.urlsafe_b64decode(salt)
   -1    45     params = [int(p, 10) for p in params]
   -1    46     if algo == b'fernet-argon2' and len(params) == 3:
   -1    47         key = get_argon2(password, salt, *params)
   -1    48     else:
   -1    49         raise TypeError('Unknown encryption algorithm')
   -1    50     return Fernet(key).decrypt(content)

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

@@ -4,10 +4,9 @@ import os
    4     4 from dataclasses import dataclass
    5     5 from pathlib import Path
    6     6 
    7    -1 import argon2
    8    -1 from cryptography.fernet import Fernet
    9     7 from cryptography.fernet import InvalidToken
   10     8 
   -1     9 from . import crypto
   11    10 from .kernel_keyring import KernelKey
   12    11 from .prompt import PinentryPrompt as Prompt
   13    12 
@@ -40,54 +39,6 @@ def write_bytes(path: Path, data: bytes) -> int:
   40    39         os.close(fd)
   41    40 
   42    41 
   43    -1 class Crypt:
   44    -1     def __init__(self, password: bytes):
   45    -1         self.password = KernelKey(password)
   46    -1 
   47    -1     def get_argon2(
   48    -1         self,
   49    -1         salt: bytes,
   50    -1         time_cost: int,
   51    -1         memory_cost: int,
   52    -1         parallelism: int,
   53    -1     ) -> bytes:
   54    -1         # https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice
   55    -1         key = argon2.low_level.hash_secret_raw(
   56    -1             secret=self.password.value,
   57    -1             salt=salt,
   58    -1             time_cost=time_cost,
   59    -1             memory_cost=memory_cost,
   60    -1             parallelism=parallelism,
   61    -1             hash_len=32,
   62    -1             type=argon2.low_level.Type.ID,
   63    -1         )
   64    -1         return base64.urlsafe_b64encode(key)
   65    -1 
   66    -1     def encrypt(self, data: bytes) -> bytes:
   67    -1         salt = os.urandom(16)
   68    -1         params = [3, 1 << 16, 4]
   69    -1         key = self.get_argon2(salt, *params)
   70    -1         content = Fernet(key).encrypt(data)
   71    -1         return b'$'.join(
   72    -1             [
   73    -1                 b'fernet-argon2',
   74    -1                 base64.urlsafe_b64encode(salt),
   75    -1                 *[str(p).encode() for p in params],
   76    -1                 content,
   77    -1             ]
   78    -1         )
   79    -1 
   80    -1     def decrypt(self, data: bytes) -> bytes:
   81    -1         algo, salt, *params, content = data.split(b'$')
   82    -1         salt = base64.urlsafe_b64decode(salt)
   83    -1         params = [int(p, 10) for p in params]
   84    -1         if algo == b'fernet-argon2' and len(params) == 3:
   85    -1             key = self.get_argon2(salt, *params)
   86    -1         else:
   87    -1             raise TypeError('Unknown encryption algorithm')
   88    -1         return Fernet(key).decrypt(content)
   89    -1 
   90    -1 
   91    42 class Keyring:
   92    43     def __init__(self, path: Path):
   93    44         self.path = path
@@ -95,28 +46,28 @@ class Keyring:
   95    46 
   96    47         if self.path.exists():
   97    48             while True:
   98    -1                 self.crypt = self._get_crypt()
   -1    49                 self.password = self._get_password()
   99    50                 try:
  100    51                     self._read()
  101    52                     break
  102    53                 except InvalidToken:
  103    54                     pass
  104    55         else:
  105    -1             self.crypt = self._get_crypt()
   -1    56             self.password = self._get_password()
  106    57             self._write({})
  107    58 
  108    -1     def _get_crypt(self):
   -1    59     def _get_password(self):
  109    60         # TODO: different messages for create|unlock|retry
  110    61         password = self.prompt.get_password(
  111    62             'An application wants access to your keyring, but it is locked'
  112    63         )
  113    64         if not password:
  114    65             raise AccessDeniedError
  115    -1         return Crypt(password)
   -1    66         return KernelKey(password)
  116    67 
  117    68     def _read(self) -> dict[int, Item]:
  118    69         encrypted = self.path.read_bytes()
  119    -1         decrypted = self.crypt.decrypt(encrypted)
   -1    70         decrypted = crypto.decrypt_with_password(encrypted, self.password.value)
  120    71         raw = json.loads(decrypted)
  121    72         return {
  122    73             id: Item(base64.urlsafe_b64decode(secret), attributes, app_id)
@@ -134,7 +85,7 @@ class Keyring:
  134    85             for id, item in items.items()
  135    86         ]
  136    87         decrypted = json.dumps(raw).encode('utf-8')
  137    -1         encrypted = self.crypt.encrypt(decrypted)
   -1    88         encrypted = crypto.encrypt_with_password(decrypted, self.password.value)
  138    89         write_bytes(self.path, encrypted)
  139    90 
  140    91     def confirm_access(self, app_id: str) -> None: