Created
March 13, 2026 20:26
-
-
Save sedrubal/1b6b07d36850bbbe900eb268624bee0f to your computer and use it in GitHub Desktop.
Create empty keyring databases for gnome-keyring
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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