Skip to content

Instantly share code, notes, and snippets.

@sedrubal
Last active April 10, 2026 10:38
Show Gist options
  • Select an option

  • Save sedrubal/1b6b07d36850bbbe900eb268624bee0f to your computer and use it in GitHub Desktop.

Select an option

Save sedrubal/1b6b07d36850bbbe900eb268624bee0f to your computer and use it in GitHub Desktop.
Create empty gnome keyring database files, change their password (without a running gnome-keyring-daemon) and unlock the gnome-keyring
#!/usr/bin/env python3
"""
Tool for GNOME Keyring (features that are not part of the command line tool gnome-keyring).
See also <https://gist.github.com/sedrubal/1b6b07d36850bbbe900eb268624bee0f>
"""
import argparse
import binascii
import dataclasses
import enum
import hashlib
import io
import logging
import os
import random
import socket
import struct
import sys
import typing
from datetime import datetime, timezone
from getpass import getpass
from pathlib import Path
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
#: Exit code when something was changed.
EXIT_CHANGED = 10
class EncAlgo(enum.IntEnum):
AES = 0
class HashAlgo(enum.IntEnum):
MD5 = 0
HEADER = b"GnomeKeyring\n\r\x00\n"
VERSION_MAJOR = 0
VERSION_MINOR = 0
HASH_ALGO = HashAlgo.MD5
class Flags(enum.IntEnum):
"""
See pkcs11/secret-store/gck-secret-binary.c or gnome-keyring/pkcs11/secret-store/gkm-secret-binary.c
"""
# docs says: (flag 0 == lock_on_idle)
# but this seems to be wrong
NO_FLAG = 0
LOCK_ON_IDLE = 0b01
LOCK_AFTER = 0b10
class ItemType(enum.IntEnum):
"""TODO"""
UNKNOWN_0 = 0
UNKNOWN_2 = 2
class ItemAttrType(enum.IntEnum):
"""Type of the attribute value."""
STRING = 0
class AclAllowType(enum.IntEnum):
"""TODO"""
UNKNOWN = 0
def derive_key_and_iv(
password: str, salt: bytes, iterations: int, key_len: int, iv_len: int
) -> tuple[bytes, bytes]:
assert password
assert salt
digest = b""
data = b""
while True:
# loop over passes
algo = hashlib.sha256()
if digest:
# in pass > 1: seed the hash with the digest from before
algo.update(digest)
algo.update(password.encode())
algo.update(salt)
digest = algo.digest()
for _ in range(1, iterations):
algo = hashlib.sha256()
algo.update(digest)
digest = algo.digest()
data += digest
if len(data) >= key_len + iv_len:
return data[:key_len], data[key_len : key_len + iv_len]
def md5hash(data: bytes) -> bytes:
md5 = hashlib.md5()
md5.update(data)
return md5.digest()
def encrypt_data(
plain_data: bytes, password: str, salt: bytes, iterations: int
) -> bytes:
key, iv = derive_key_and_iv(
password=password,
salt=salt,
iterations=iterations,
key_len=algorithms.AES128.key_size // 8,
iv_len=algorithms.AES128.key_size // 8,
)
cipher = Cipher(algorithms.AES128(key), modes.CBC(iv))
encryptor = cipher.encryptor()
encrypted_data = encryptor.update(plain_data) + encryptor.finalize()
return encrypted_data
EMPTY_STRING_LEN = 0xFF_FF_FF_FF
def parse_string(file: typing.BinaryIO) -> str:
"""
Parse a string.
uint32 + bytes, no padding, NULL is encoded as 0xffffffff
"""
str_len = struct.unpack(">I", file.read(4))[0]
if str_len == EMPTY_STRING_LEN:
return ""
return struct.unpack(f"{str_len}s", file.read(str_len))[0].decode("utf-8")
def parse_datetime(file: typing.BinaryIO) -> datetime:
"""
Parse a datetime.
time_t: 2 * guint32
"""
return datetime.fromtimestamp(struct.unpack(">Q", file.read(8))[0])
def dump_string(value: str, file: typing.BinaryIO) -> None:
"""Dump a string into a file."""
if not value:
file.write(struct.pack(">I", EMPTY_STRING_LEN))
return
file.write(struct.pack(f">I{len(value)}s", len(value), value.encode()))
def dump_datetime(value: datetime, file: typing.BinaryIO) -> None:
"""Dump a datetime into a file."""
file.write(struct.pack(">Q", int(value.timestamp())))
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
class ItemInfo:
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
class Attribute:
name: str
attr_type: ItemAttrType
attr_hash: int | str
@classmethod
def parse(cls, file: typing.BinaryIO) -> typing.Self:
name = parse_string(file)
attr_type = ItemAttrType(struct.unpack(">I", file.read(4))[0])
if attr_type == ItemAttrType.STRING:
attr_hash = parse_string(file)
else:
attr_hash = struct.unpack(">I", file.read(4))[0]
return cls(name=name, attr_type=attr_type, attr_hash=attr_hash)
def dump(self, file: typing.BinaryIO) -> None:
dump_string(self.name, file=file)
file.write(struct.pack(">I", self.attr_type))
if self.attr_type == ItemAttrType.STRING:
assert isinstance(self.attr_hash, str)
dump_string(self.attr_hash, file=file)
else:
assert False, f"Unsupported attribute type {self.attr_type}"
def __str__(self) -> str:
return f"{self.name}={self.attr_hash!r}"
item_id: int
item_type: ItemType
attributes: "list[ItemInfo.Attribute]"
@classmethod
def parse(cls, file: typing.BinaryIO) -> typing.Self:
item_id = struct.unpack(">I", file.read(4))[0]
item_type = ItemType(struct.unpack(">I", file.read(4))[0])
num_attributes = struct.unpack(">I", file.read(4))[0]
attributes: list[ItemInfo.Attribute] = []
for _ in range(num_attributes):
attributes.append(ItemInfo.Attribute.parse(file))
return cls(item_id=item_id, item_type=item_type, attributes=attributes)
def dump(self, file: typing.BinaryIO) -> None:
"""Dump into file."""
file.write(
struct.pack(">III", self.item_id, self.item_type, len(self.attributes))
)
for attr in self.attributes:
attr.dump(file=file)
def __str__(self) -> str:
attrs_str = ", ".join(str(attr) for attr in self.attributes)
return f"Item ID: {self.item_id} Item Type: {self.item_type} Attrs: {attrs_str}"
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
class Item:
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
class Attribute:
name: str
attr_type: ItemAttrType
value: int | str
@classmethod
def parse(cls, file: typing.BinaryIO) -> typing.Self:
name = parse_string(file)
attr_type = ItemAttrType(struct.unpack(">I", file.read(4))[0])
if attr_type == ItemAttrType.STRING:
value = parse_string(file)
else:
value = struct.unpack(">I", file.read(4))[0]
return cls(name=name, attr_type=attr_type, value=value)
def dump(self, file: typing.BinaryIO) -> None:
dump_string(self.name, file=file)
file.write(struct.pack(">I", self.attr_type))
if self.attr_type == ItemAttrType.STRING:
assert isinstance(self.value, str)
dump_string(self.value, file=file)
else:
assert False, f"Unsupported attribute type {self.attr_type}"
def __str__(self) -> str:
return f"{self.name}={self.value!r}"
def get_attr_info(self) -> ItemInfo.Attribute:
assert self.attr_type == ItemAttrType.STRING, (
"Can only generate attr info from string attr type."
)
assert isinstance(self.value, str)
return ItemInfo.Attribute(
name=self.name,
attr_type=self.attr_type,
attr_hash=binascii.hexlify(
md5hash(self.value.encode("utf-8"))
).decode(),
)
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
class Acl:
types_allowed: AclAllowType
display_name: str
pathname: str
# reserved_str: str
# reserved_uint: int
@classmethod
def parse(cls, file: typing.BinaryIO) -> typing.Self:
types_allowed = AclAllowType(struct.unpack(">I", file.read(4))[0])
display_name = parse_string(file)
pathname = parse_string(file)
_reserved_str = parse_string(file)
_reserved_uint = struct.unpack(">I", file.read(4))[0]
return cls(
types_allowed=types_allowed,
display_name=display_name,
pathname=pathname,
)
def dump(self, file: typing.BinaryIO) -> None:
file.write(struct.pack(">I", self.types_allowed))
dump_string(self.display_name, file=file)
dump_string(self.pathname, file=file)
dump_string("", file=file) # reserved_str
file.write(struct.pack(">I", 0)) # reserved_uint
display_name: str
secret: str
ctime: datetime
mtime: datetime
# reserved_str: str
# reserved_int2: tuple[int, int, int, int]
attributes: "list[Item.Attribute]"
acls: "list[Item.Acl]"
@classmethod
def parse(cls, file: typing.BinaryIO) -> typing.Self:
display_name = parse_string(file)
secret = parse_string(file)
ctime = parse_datetime(file)
mtime = parse_datetime(file)
_reserved_str = parse_string(file)
_reserved_int2 = [
struct.unpack(">I", file.read(4))[0],
struct.unpack(">I", file.read(4))[0],
struct.unpack(">I", file.read(4))[0],
struct.unpack(">I", file.read(4))[0],
]
num_attributes = struct.unpack(">I", file.read(4))[0]
attributes: list[Item.Attribute] = []
for _ in range(num_attributes):
attributes.append(Item.Attribute.parse(file))
acl_len = struct.unpack(">I", file.read(4))[0]
acls: list[Item.Acl] = []
for _ in range(acl_len):
acls.append(Item.Acl.parse(file))
return cls(
display_name=display_name,
secret=secret,
ctime=ctime,
mtime=mtime,
# reserved_str=reserved_str,
# reserved_int2=reserved_int2,
attributes=attributes,
acls=acls,
)
def dump(self, file: typing.BinaryIO) -> None:
dump_string(self.display_name, file=file)
dump_string(self.secret, file=file)
dump_datetime(self.ctime, file=file)
dump_datetime(self.mtime, file=file)
dump_string("", file=file) # reserved_str
file.write(struct.pack(">IIII", 0, 0, 0, 0)) # reserved_int
file.write(struct.pack(">I", len(self.attributes)))
for attr in self.attributes:
attr.dump(file=file)
file.write(struct.pack(">I", len(self.acls)))
for acl in self.acls:
acl.dump(file=file)
def __str__(self) -> str:
attrs_str = ", ".join(str(attr) for attr in self.attributes)
acls_str = ", ".join(str(acl) for acl in self.acls)
return f"Display Name: {self.display_name!r} Secret: {self.secret!r} CTime: {self.ctime} MTime: {self.mtime} Attrs: {attrs_str} ACLs: {acls_str}"
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
class Vault:
"""
The content of a decrypted vault file.
Docs for the binary format:
- https://wiki.gnome.org/Projects(2f)GnomeKeyring(2f)KeyringFormats(2f)FileFormat.html
- https://github.com/GNOME/gnome-keyring/blob/main/docs/file-format.txt
"""
name: str
mtime: datetime
ctime: datetime
flags: Flags
timeout: int
items: list[Item]
hash_iterations: int
salt: bytes
@classmethod
def parse(cls, file: typing.BinaryIO, password: str) -> typing.Self:
header = struct.unpack("16s", file.read(16))[0]
assert header == HEADER, f"{header!r} != {HEADER!r}"
(version_major, version_minor, enc_algo, hash_algo) = struct.unpack(
"bbbb", file.read(4)
)
assert (version_major, version_minor, enc_algo, hash_algo) == (
VERSION_MAJOR,
VERSION_MINOR,
EncAlgo.AES.value,
HASH_ALGO,
)
name = parse_string(file)
mtime = parse_datetime(file)
ctime = parse_datetime(file)
flags = Flags(struct.unpack(">I", file.read(4))[0])
timeout = struct.unpack(">I", file.read(4))[0]
hash_iterations = struct.unpack(">I", file.read(4))[0]
salt = struct.unpack("8s", file.read(8))[0]
for _ in range(4):
# reserved 4 * 32bit
_padding = file.read(4)
num_items = struct.unpack(">I", file.read(4))[0]
item_infos = [ItemInfo.parse(file=file) for _ in range(num_items)]
num_encrypted_bytes = struct.unpack(">I", file.read(4))[0]
encrypted_data = struct.unpack(
f"{num_encrypted_bytes}s", file.read(num_encrypted_bytes)
)[0]
eof = file.read()
assert eof == b"", (
f"File should already be completely read. There was trash at the end: {eof!r}"
)
plain_data = decrypt_data(
encrypted_data=encrypted_data,
password=password,
salt=salt,
iterations=hash_iterations,
)
assert len(plain_data) % 16 == 0, (
"Decrypted data is not zero padded to be a multiple of 16"
)
decrypted_data_hash = plain_data[:16]
plain_data = plain_data[16:]
assert HASH_ALGO == HashAlgo.MD5
calculated_data_hash = md5hash(plain_data)
if decrypted_data_hash != calculated_data_hash:
raise ValueError("Verification failed. Is the password correct?")
plain_data_io = io.BytesIO(initial_bytes=plain_data)
items = [Item.parse(file=plain_data_io) for _ in range(num_items)]
for item_info, item in zip(item_infos, items):
assert len(item.attributes) == len(item_info.attributes), (
f"Amount of item info attributes is not equal to amount of item attributes: {item.attributes=} != {item_info.attributes=}"
)
for attr, attr_info in zip(item.attributes, item_info.attributes):
calculated_attr_info = attr.get_attr_info()
assert calculated_attr_info == attr_info, (
f"Item attribute information does not match expected attribute info: {calculated_attr_info!r} != {attr_info!r}"
)
# zero padding to make even multiple of 16
real_len_enc_data = plain_data_io.tell()
zero_padding = plain_data_io.read()
assert zero_padding == b"\00" * (plain_data_io.tell() - real_len_enc_data), (
f"Invalid zero padding at the end of the decrypted data block: {zero_padding!r}"
)
return cls(
name=name,
mtime=mtime,
ctime=ctime,
flags=flags,
timeout=timeout,
items=items,
hash_iterations=hash_iterations,
salt=salt,
)
def dump(self, file: typing.BinaryIO, password: str) -> None:
file.write(HEADER)
file.write(
struct.pack(
"bbbb",
VERSION_MAJOR,
VERSION_MINOR,
EncAlgo.AES.value,
HASH_ALGO,
)
)
assert len(self.name) <= 32, "Name is too long"
assert self.name, (
"Name is required. I think, if it is empty, it must be set to 0xffffffff"
)
dump_string(self.name, file=file)
dump_datetime(self.mtime, file=file)
dump_datetime(self.ctime, file=file)
file.write(struct.pack(">I", self.flags))
file.write(struct.pack(">I", self.timeout))
file.write(struct.pack(">I", self.hash_iterations))
file.write(struct.pack("8s", self.salt))
for _ in range(4):
# reserved 4 * 32bit
file.write(struct.pack("xxxx"))
file.write(struct.pack(">I", len(self.items)))
for idx, item in enumerate(self.items):
item_info = ItemInfo(
item_id=idx,
item_type=ItemType.UNKNOWN_0,
attributes=[attr.get_attr_info() for attr in item.attributes],
)
item_info.dump(file=file)
plain_data_io = io.BytesIO(initial_bytes=b"")
for item in self.items:
item.dump(file=plain_data_io)
# zero padding
if plain_data_io.tell() % 16 != 0:
plain_data_io.write(b"\x00" * (16 - (plain_data_io.tell() % 16)))
plain_data = plain_data_io.getvalue()
assert HASH_ALGO == HashAlgo.MD5
data_hash = md5hash(plain_data)
data_to_encrypt = data_hash + plain_data
encrypted_data = encrypt_data(
plain_data=data_to_encrypt,
password=password,
salt=self.salt,
iterations=self.hash_iterations,
)
num_encrypted_bytes = len(encrypted_data)
file.write(struct.pack(">I", num_encrypted_bytes))
file.write(struct.pack(f"{num_encrypted_bytes}s", encrypted_data))
def decrypt_data(
encrypted_data: bytes, password: str, salt: bytes, iterations: int
) -> bytes:
key, iv = derive_key_and_iv(
password=password,
salt=salt,
iterations=iterations,
key_len=algorithms.AES128.key_size // 8,
iv_len=algorithms.AES128.key_size // 8,
)
cipher = Cipher(algorithms.AES128(key), modes.CBC(iv))
decryptor = cipher.decryptor()
plain_data = decryptor.update(encrypted_data) + decryptor.finalize()
return plain_data
def create_keyring(file_path: Path, vault: Vault, password: str) -> None:
with file_path.open("wb") as file:
vault.dump(file=file, password=password)
def verify_keyring(file_path: Path, password: str) -> None:
"""Verify the password and the content of a keyring."""
with file_path.open("rb") as file:
vault = Vault.parse(file, password=password)
logging.info("Name: %s", vault.name)
logging.info("MTime: %s", vault.mtime)
logging.info("CTime: %s", vault.ctime)
logging.info("Flags: %s", vault.flags)
logging.info("Timeout: %s", vault.timeout)
logging.info("Hash Iterations: %i", vault.hash_iterations)
logging.info("Salt: %s", binascii.hexlify(vault.salt).decode("utf-8"))
for item in vault.items:
logging.info("- %s", item)
logging.info("Looks good")
def change_password(file_path: Path, old_password: str, new_password: str) -> None:
"""Change the vault password."""
with file_path.open("rb") as file:
vault = Vault.parse(file, password=old_password)
with file_path.open("wb") as file:
vault.dump(file, password=new_password)
def force_password(file_path: Path, password: str) -> None:
"""Change the vault password."""
try:
with file_path.open("rb") as file:
vault = Vault.parse(file, password=password)
logging.info("The password matches. Nothing was changed.")
sys.exit(0)
except (ValueError, FileNotFoundError) as err:
if isinstance(err, ValueError):
logging.warning("Password did not match. Creating a new empty vault.")
elif isinstance(err, FileNotFoundError):
logging.warning(
"Wallet %s does not exist. Creating a new empty vault.", file_path
)
backup_path = file_path.parent / f"{file_path.name}.bak"
if file_path.exists() and not backup_path.exists():
logging.info(" Backing up old vault to %s", backup_path)
file_path.rename(backup_path)
vault = Vault(
name=file_path.stem,
mtime=datetime.fromtimestamp(0, tz=timezone.utc),
ctime=datetime.now(tz=timezone.utc),
flags=Flags.NO_FLAG,
timeout=0,
hash_iterations=random.randint(3500, 5000),
salt=random.randbytes(8),
items=[],
)
with file_path.open("wb") as file:
vault.dump(file, password=password)
sys.exit(EXIT_CHANGED)
def set_default_keyring(value: str, default_path: Path) -> None:
"""Set the default keyring."""
known_keyrings = [
file_path.name[: -len(".keyring")]
for file_path in default_path.parent.glob("*.keyring")
]
if value not in known_keyrings:
logging.warning(
"Keyring %s can't be found. Setting it anyway. Known keyrings are %s",
", ".join(known_keyrings),
)
default_keyring = get_default_keyring(default_path=default_path)
if default_keyring != value:
logging.info(
"Changing the default keyring from %s to %s.", default_keyring, value
)
with default_path.open("w") as file:
print(value, file=file)
sys.exit(EXIT_CHANGED)
else:
logging.info("Keyring %s already set as default", value)
def get_default_keyring(default_path: Path) -> str | None:
"""Get the default keyring."""
if not default_path.is_file():
return None
with default_path.open("r") as file:
default_keyring = file.read().strip()
return default_keyring
def get_default_action(default_path: Path) -> None:
"""The command line action for getting the default keyring."""
default_keyring = get_default_keyring(default_path=default_path)
if default_keyring:
print(default_keyring)
else:
logging.warning("No default keyring is currently set.")
def unit_tests():
"""Unit tests^^."""
key, iv = derive_key_and_iv(
password="booo",
salt=b"\x27\x55\xc6\x12\xfe\x71\xe1\xb8",
iterations=2678,
key_len=algorithms.AES128.key_size // 8,
iv_len=algorithms.AES128.key_size // 8,
)
assert key == b"\x63\x8a\x25\xe1\x1b\x69\xb9\xcd\x41\x73\x9d\x5f\x20\x43\x03\x65"
assert iv == b"\x35\x40\x47\xe6\x3b\x00\x2e\x66\x84\x22\x71\x20\x67\x2e\x66\xa3"
assert HASH_ALGO == HashAlgo.MD5
verifier_data = md5hash(b"")
real_verifier = b"\xd4\x1d\x8c\xd9\x8f\x00\xb2\x04\xe9\x80\x09\x98\xec\xf8\x42\x7e"
assert verifier_data == real_verifier
attr = Item.Attribute(name="a name", attr_type=ItemAttrType.STRING, value="a value")
acl = Item.Acl(
types_allowed=AclAllowType.UNKNOWN,
display_name="display-name",
pathname="path/name",
)
item = Item(
display_name="display name",
secret="secret",
ctime=datetime.fromtimestamp(0),
mtime=datetime.fromtimestamp(0),
attributes=[attr],
acls=[acl],
)
for item in (attr, attr.get_attr_info(), acl, item):
buffer = io.BytesIO()
item.dump(file=buffer)
buffer.seek(0)
assert item == item.__class__.parse(file=buffer)
logging.info("Passed!")
class ControlOpCode(enum.IntEnum):
"""Op codes for the control socket."""
INITIALIZE = 0
UNLOCK = 1
CHANGE = 2
QUIT = 4
class ControlResult(enum.IntEnum):
"""Result codes for the control sockets."""
OK = 0
DENIED = 1
FAILED = 2
NO_DAEMON = 3
def get_control_socket_path() -> Path:
"""Find the control socket for gnome keyring."""
if "GNOME_KEYRING_CONTROL" in os.environ:
socket_path = Path(os.environ["GNOME_KEYRING_CONTROL"]) / "control"
if socket_path.exists() and socket_path.is_socket():
return socket_path
if "XDG_RUNTIME_DIR" in os.environ:
socket_path = Path(os.environ["XDG_RUNTIME_DIR"]) / "keyring" / "control"
if socket_path.exists() and socket_path.is_socket():
return socket_path
raise FileNotFoundError("Unable to find control socket.")
def unlock_keyring(socket_path: Path, password: str) -> None:
"""
Unlock the default gnome keyring (communicates with gnome-keyring-daemon via the control socket).
The code is based on
<https://unix.stackexchange.com/a/723048/95359> which points to
<https://codeberg.org/umglurf/gnome-keyring-unlock/raw/branch/main/unlock.py>
"""
sock = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)
sock.connect(str(socket_path))
def recv_int() -> int:
data = sock.recv(4)
return struct.unpack(">I", data)[0]
pw_len = len(password)
# op_len is
# 4 (uint32): bytes packet size
# 4 (uint32): bytes for op code
# 4 (uint32): bytes length of password
# length of password (char): password
op_len = 3 * 4 + pw_len
msg = struct.pack(
f">BIII{pw_len}s",
0,
op_len,
ControlOpCode.UNLOCK,
pw_len,
password.encode(),
)
num_sent_bytes = sock.send(msg)
if num_sent_bytes != len(msg):
raise Exception(
"Error communicating with gnome-keyring control: "
f"Tried to send {msg!r}, but could only send {num_sent_bytes} bytes."
)
response_length = recv_int()
if response_length != 8:
raise Exception("Invalid response length")
result = ControlResult(recv_int())
sock.close()
match result:
case ControlResult.DENIED:
raise Exception("Unlock denied! Was the password correct?")
case ControlResult.FAILED:
raise Exception("Unlock failed!")
case ControlResult.OK:
logging.info("Successfully unlocked keyring.")
case _:
raise Exception(f"Unexpected gnome-keyring control socket result {result}")
def get_password(prompt: str) -> str:
"""Get a password from stdin with fallback, when turning of echo does not work."""
if sys.stdin.isatty():
return getpass(prompt=prompt)
else:
return input(prompt)
def parse_args() -> argparse.Namespace:
"""Parse command line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
def password_argument(value: str) -> str:
if value.startswith("file:"):
password_file = Path(value[len("file:") :])
try:
with password_file.open("r") as file:
return file.read().rstrip("\n\r")
except OSError as err:
raise argparse.ArgumentTypeError(
f"Could not read password file {password_file}: {err}"
) from err
else:
return value
try:
default_control_socket = get_control_socket_path()
except FileNotFoundError as exc:
default_control_socket = None
logging.debug(str(exc))
subparsers = parser.add_subparsers(
dest="action",
description="Choose the action you want to execute.",
required=True,
help="the action to execute",
)
create_subparser = subparsers.add_parser(
name="create",
description="Create a new empty keyring",
help="Create a new empty keyring with a given password.",
)
create_subparser.add_argument(
"-n",
"--name",
type=str,
action="store",
default=None,
help="The name of the keyring to create (default: derived from the file name)..",
)
verify_subparser = subparsers.add_parser(
name="verify",
description="Verify the password of a keyring",
help="Verify that the password of a keyring matches.",
)
change_pw_subparser = subparsers.add_parser(
name="change-password",
description="Change the password of a keyring",
help="Change the password of a keyring.",
)
unlock_keyring_subparser = subparsers.add_parser(
name="unlock-keyring",
description="Unlock a keyring",
help="Unlock a keyring. Communicates with the gnome-keyring-daemon.",
)
unlock_keyring_subparser.add_argument(
"-s",
"--socket",
dest="socket_path",
type=Path,
action="store",
default=default_control_socket,
required=default_control_socket is None,
help="The gnome-keyring daemon control socket (default %(default)s).",
)
force_password_subparser = subparsers.add_parser(
name="force-password",
description="Create a new keyring if the desire password does not match",
help="Check if the desired password matches. If not overwrite the keyring with an empty one. Data might get lost.",
)
force_password_subparser.add_argument(
"--i-know-what-i-am-doing",
action="store_true",
help="Use this if you are aware, that data might get lost.",
)
set_default_subparser = subparsers.add_parser(
name="set-default",
description="Set the default keyring",
help="Set the default keyring for a user.",
)
set_default_subparser.add_argument(
"keyring",
type=str,
help="The name of the default keyring (e.g. 'login').",
)
get_default_subparser = subparsers.add_parser(
name="get-default",
description="Get the default keyring",
help="Get the default keyring of a user.",
)
for subparser in (set_default_subparser, get_default_subparser):
subparser.add_argument(
"-f",
"--default-file",
type=Path,
default=Path("~/.local/share/keyrings/default").expanduser(),
help="The path to the file, where the name of the default keyring is saved (default: %(default)s).",
)
for subparser in (
create_subparser,
verify_subparser,
change_pw_subparser,
force_password_subparser,
):
subparser.add_argument(
"file_path",
type=Path,
help="The path to the keyring.",
)
for subparser, short_arg, long_arg, help in (
(create_subparser, None, None, None),
(verify_subparser, None, None, None),
(force_password_subparser, None, None, None),
(unlock_keyring_subparser, None, None, None),
(
change_pw_subparser,
"-op",
"--old-password",
"The old password of the keyring. Use --old-password=file:./path/to/file to load from file. (default: ask).",
),
(
change_pw_subparser,
"-np",
"--new-password",
"The new password of the keyring. Use --old-password=file:./path/to/file to load from file. (default: ask).",
),
):
subparser.add_argument(
short_arg or "-p",
long_arg or "--password",
type=password_argument,
action="store",
help=help
or "The password for the keyring. Use --password=file:./path/to/file to load from file. (default: ask).",
)
_test_subparser = subparsers.add_parser(
name="test",
description="Internal unit tests",
help="Internal unit tests / self-checks.",
)
parser.add_argument(
"-v",
"--verbose",
dest="log_level",
action="store_const",
const=logging.DEBUG,
default=logging.INFO,
help="Log debug messages.",
)
return parser.parse_args()
def main():
args = parse_args()
logger = logging.getLogger()
logger.setLevel(args.log_level)
handler = logging.StreamHandler()
handler.setLevel(args.log_level)
logger.addHandler(handler)
match args.action:
case "create":
create_keyring(
file_path=args.file_path,
vault=Vault(
name=args.name or args.file_path.stem,
mtime=datetime.fromtimestamp(0, tz=timezone.utc),
ctime=datetime.now(tz=timezone.utc),
flags=Flags.NO_FLAG,
timeout=0,
hash_iterations=random.randint(3500, 5000),
salt=random.randbytes(8),
items=[],
),
password=(args.password or get_password("Please enter the password: ")),
)
case "verify":
verify_keyring(
file_path=args.file_path,
password=(args.password or get_password("Please enter the password: ")),
)
case "change-password":
change_password(
file_path=args.file_path,
old_password=(
args.old_password or get_password("Please enter the old password: ")
),
new_password=(
args.new_password or get_password("Please enter the new password: ")
),
)
case "force-password":
if not args.i_know_what_i_am_doing:
logger.fatal(
"This will overwrite the current keyring and all secrets might get lost! Are you sure? Use --i-know-what-i-am-doing."
)
sys.exit(1)
force_password(
file_path=args.file_path,
password=(
args.password or get_password("Please enter the new password: ")
),
)
case "unlock-keyring":
unlock_keyring(
socket_path=args.socket_path,
password=args.password or get_password("Please enter the password: "),
)
case "set-default":
set_default_keyring(args.keyring, default_path=args.default_file)
case "get-default":
get_default_action(default_path=args.default_file)
case "test":
unit_tests()
case _:
assert False
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment