Skip to content

Instantly share code, notes, and snippets.

@sedrubal
Created March 13, 2026 20:26
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 keyring databases for gnome-keyring
#!/usr/bin/env python3
"""
See https://wiki.gnome.org/Projects(2f)GnomeKeyring(2f)KeyringFormats(2f)FileFormat.html
Or
https://github.com/GNOME/gnome-keyring/blob/main/docs/file-format.txt
"""
import enum
import hashlib
import struct
import sys
from datetime import datetime, timezone
from pathlib import Path
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
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
# NAME = "tmp"
# # NAME = "Login"
# TIMEOUT = 0
# HASH_ITERATIONS = random.randint(1000, 4096)
# SALT = random.randbytes(8)
NUM_ITEMS = 0
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
# def pad(data: bytes, modulo: int, fill_byte: bytes = b"\x00") -> bytes:
# filled = len(data) % modulo
#
# if filled == 0:
# return
# return data + (fill_byte * (modulo - filled))
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()
print("enc", key, iv)
print(f"{encrypted_data=}")
return encrypted_data
# decryptor = cipher.decryptor()
# return decryptor.update(ct) + decryptor.finalize()
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,
)
print("enc", key, iv)
print(f"{encrypted_data=}")
cipher = Cipher(algorithms.AES128(key), modes.CBC(iv))
decryptor = cipher.decryptor()
plain_data = decryptor.update(encrypted_data) + decryptor.finalize()
print(f"{plain_data=}")
return plain_data
def create_keyring(
file_path: Path,
name: str,
mtime: datetime,
ctime: datetime,
flags: Flags,
timeout: int,
hash_iterations: int,
salt: bytes,
password: str,
) -> None:
with file_path.open("wb") as file:
file.write(HEADER)
file.write(
struct.pack(
"bbbb",
VERSION_MAJOR,
VERSION_MINOR,
EncAlgo.AES.value,
HASH_ALGO,
)
)
assert len(name) <= 32, "Name is too long"
assert name, (
"Name is required. I think, if it is empty, it must be set to 0xffffffff"
)
file.write(struct.pack(f">I{len(name)}s", len(name), name.encode()))
file.write(struct.pack(">Q", int(mtime.timestamp())))
file.write(struct.pack(">Q", int(ctime.timestamp())))
file.write(struct.pack(">I", flags))
file.write(struct.pack(">I", timeout))
file.write(struct.pack(">I", hash_iterations))
file.write(struct.pack("8s", salt))
for _ in range(4):
# reserved 4 * 32bit
file.write(struct.pack("xxxx"))
file.write(struct.pack(">I", NUM_ITEMS))
# num items times something -> here nothing
plain_data = b""
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=salt,
iterations=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 verify_keyring(file_path: Path, password: str) -> None:
with file_path.open("rb") as file:
assert struct.unpack("16s", file.read(16)), HEADER
assert struct.unpack("bbbb", file.read(4)), (
VERSION_MAJOR,
VERSION_MINOR,
EncAlgo.AES.value,
HASH_ALGO,
)
name_len = struct.unpack(">I", file.read(4))[0]
name = struct.unpack(f"{name_len}s", file.read(name_len))[0]
print(f"{name=}")
mtime = datetime.fromtimestamp(struct.unpack(">Q", file.read(8))[0])
print(f"{mtime=}")
ctime = datetime.fromtimestamp(struct.unpack(">Q", file.read(8))[0])
print(f"{ctime=}")
flags = Flags(struct.unpack(">I", file.read(4))[0])
print(f"{flags=}")
timeout = struct.unpack(">I", file.read(4))[0]
print(f"{timeout=}")
hash_iterations = struct.unpack(">I", file.read(4))[0]
print(f"{hash_iterations=}")
salt = struct.unpack("8s", file.read(8))[0]
print(f"{salt=}")
for _ in range(4):
_padding = file.read(4)
num_items = struct.unpack(">I", file.read(4))[0]
print(f"{num_items=}")
if num_items > 0:
print("WARNING: Only empty databases are currently supported. There will be an error now.")
num_encrypted_bytes = struct.unpack(">I", file.read(4))[0]
print(f"{num_encrypted_bytes=}")
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}"
)
plain_data = decrypt_data(encrypted_data=encrypted_data, password=password, salt=salt, iterations=hash_iterations)
decrypted_data_hash = plain_data[:16]
plain_data = plain_data[16:]
assert HASH_ALGO == HashAlgo.MD5
calculated_data_hash = md5hash(plain_data)
assert decrypted_data_hash == calculated_data_hash, f"{decrypted_data_hash=} != {calculated_data_hash=}"
print(f"{plain_data=}")
print("Looks good")
def main():
action, file_path_str = sys.argv[1:]
file_path = Path(file_path_str)
password=input("please enter the password: ")
if action == "create":
create_keyring(
file_path=file_path,
name="tmp",
mtime=datetime.fromtimestamp(0),
ctime=datetime(
year=2026,
month=3,
day=13,
hour=15,
minute=48,
second=1,
tzinfo=timezone.utc,
),
flags=Flags.NO_FLAG,
timeout=0,
hash_iterations=3587,
salt=b"\x96\x4c\x54\x6b\xf3\x5e\xd3\x77",
password=password,
)
# create_keyring(
# file_path=Path(sys.argv[1]),
# name=NAME,
# mtime=datetime.fromtimestamp(0),
# ctime=datetime.now(),
# flags=Flags.NO_FLAG,
# timeout=TIMEOUT,
# hash_iterations=HASH_ITERATIONS,
# salt=SALT,
# password=PASSWORD,
# )
elif action == "verify":
verify_keyring(
file_path=file_path,
password=password,
)
elif action == "test":
# unit test^^
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"
print(key, iv)
assert HASH_ALGO == HashAlgo.MD5
verifier_data = md5hash(b"")
real_verifyer = (
b"\xd4\x1d\x8c\xd9\x8f\x00\xb2\x04\xe9\x80\x09\x98\xec\xf8\x42\x7e"
)
assert verifier_data == real_verifyer
else:
assert False
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment