#!/usr/bin/env python3 """ Headless Secret Service D-Bus Daemon Implements org.freedesktop.secrets with Plain and DH crypto support. Persists credentials to ~/.local/share/antigravity-keyring.json Bidirectionally syncs with ~/.gemini/antigravity-cli/antigravity-oauth-token """ import os import sys import json import time import uuid import hmac import math import dbus import dbus.service import dbus.mainloop.glib from hashlib import sha256 from gi.repository import GLib from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend BUS_NAME = "org.freedesktop.secrets" SS_PATH = "/org/freedesktop/secrets" SS_SERVICE_IFACE = "org.freedesktop.Secret.Service" SS_COLLECTION_IFACE = "org.freedesktop.Secret.Collection" SS_ITEM_IFACE = "org.freedesktop.Secret.Item" SS_SESSION_IFACE = "org.freedesktop.Secret.Session" SS_PROMPT_IFACE = "org.freedesktop.Secret.Prompt" DBUS_PROP_IFACE = "org.freedesktop.DBus.Properties" DATA_FILE = os.path.expanduser("~/.local/share/antigravity-keyring.json") AGY_TOKEN_FILE = os.path.expanduser("~/.gemini/antigravity-cli/antigravity-oauth-token") # Standard DH 1024-bit prime DH_PRIME_1024_BYTES = ( 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, 0x21, 0x68, 0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, 0x29, 0x02, 0x4E, 0x08, 0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A, 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, 0x30, 0x2B, 0x0A, 0x6D, 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, 0xC2, 0x45, 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9, 0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, 0xEE, 0x38, 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, 0x7C, 0x4B, 0x1F, 0xE6, 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE6, 0x53, 0x81, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF ) DH_PRIME_1024 = int.from_bytes(DH_PRIME_1024_BYTES, "big") def int_to_bytes(number: int) -> bytes: return number.to_bytes(math.ceil(number.bit_length() / 8), "big") def sync_from_agy_token_file(data): if os.path.exists(AGY_TOKEN_FILE): try: with open(AGY_TOKEN_FILE, "r") as f: token_content = f.read().strip() if token_content: items = data.setdefault("collections", {}).setdefault("login", {}).setdefault("items", {}) found = False for item_id, idata in items.items(): attrs = idata.get("attributes", {}) if attrs.get("service") == "antigravity" and attrs.get("username") == "token": if idata.get("secret") != token_content: idata["secret"] = token_content idata["modified"] = int(time.time()) found = True break if not found: items["antigravity_oauth_token"] = { "label": "Antigravity CLI OAuth Token", "attributes": { "service": "antigravity", "username": "token" }, "secret": token_content, "created": int(time.time()), "modified": int(time.time()) } except Exception as e: print(f"Error syncing from agy token file: {e}", file=sys.stderr) return data def sync_to_agy_token_file(secret_str): try: if secret_str: os.makedirs(os.path.dirname(AGY_TOKEN_FILE), exist_ok=True) tmp_file = AGY_TOKEN_FILE + ".tmp" with open(tmp_file, "w") as f: f.write(secret_str) os.replace(tmp_file, AGY_TOKEN_FILE) os.chmod(AGY_TOKEN_FILE, 0o600) except Exception as e: print(f"Error syncing to agy token file: {e}", file=sys.stderr) def load_data(): data = {"collections": {"login": {"label": "Login", "items": {}}}} if os.path.exists(DATA_FILE): try: with open(DATA_FILE, "r") as f: data = json.load(f) except Exception as e: print(f"Error loading data file: {e}", file=sys.stderr) return sync_from_agy_token_file(data) def save_data(data): os.makedirs(os.path.dirname(DATA_FILE), exist_ok=True) temp_file = DATA_FILE + ".tmp" with open(temp_file, "w") as f: json.dump(data, f, indent=2) os.replace(temp_file, DATA_FILE) os.chmod(DATA_FILE, 0o600) class Session(dbus.service.Object): def __init__(self, bus, service, session_id, algorithm, client_key_bytes=None): self.bus = bus self.service = service self.session_id = session_id self.algorithm = algorithm self.path = f"{SS_PATH}/session/{session_id}" self.aes_key = None self.server_public_key_bytes = b"" if algorithm == "dh-ietf1024-sha256-aes128-cbc-pkcs7" and client_key_bytes: my_private_key = int.from_bytes(os.urandom(0x80), "big") my_public_key = pow(2, my_private_key, DH_PRIME_1024) self.server_public_key_bytes = int_to_bytes(my_public_key) self.server_public_key_bytes = b"\x00" * (0x80 - len(self.server_public_key_bytes)) + self.server_public_key_bytes client_pub_int = int.from_bytes(bytes(client_key_bytes), "big") common_secret_int = pow(client_pub_int, my_private_key, DH_PRIME_1024) common_secret = int_to_bytes(common_secret_int) common_secret = b"\x00" * (0x80 - len(common_secret)) + common_secret salt = b"\x00" * 0x20 pseudo_random_key = hmac.new(salt, common_secret, sha256).digest() output_block = hmac.new(pseudo_random_key, b"\x01", sha256).digest() self.aes_key = output_block[:0x10] super().__init__(bus, self.path) def decrypt_secret(self, secret_struct): _, iv, encrypted_bytes, _ = secret_struct iv = bytes(iv) encrypted_bytes = bytes(encrypted_bytes) if not self.aes_key or not iv or not encrypted_bytes: return encrypted_bytes try: cipher = Cipher(algorithms.AES(self.aes_key), modes.CBC(iv), default_backend()) decryptor = cipher.decryptor() padded = decryptor.update(encrypted_bytes) + decryptor.finalize() pad_len = padded[-1] if 1 <= pad_len <= 16 and padded.endswith(bytes([pad_len]) * pad_len): return padded[:-pad_len] return padded except Exception as e: print(f"Decryption error: {e}", file=sys.stderr) return encrypted_bytes def encrypt_secret(self, raw_bytes): if not self.aes_key: return (dbus.ObjectPath(self.path), dbus.ByteArray(b""), dbus.ByteArray(raw_bytes), dbus.String("text/plain")) try: iv = os.urandom(16) pad_len = 16 - (len(raw_bytes) % 16) padded = raw_bytes + bytes([pad_len]) * pad_len cipher = Cipher(algorithms.AES(self.aes_key), modes.CBC(iv), default_backend()) encryptor = cipher.encryptor() encrypted = encryptor.update(padded) + encryptor.finalize() return (dbus.ObjectPath(self.path), dbus.ByteArray(iv), dbus.ByteArray(encrypted), dbus.String("text/plain")) except Exception as e: print(f"Encryption error: {e}", file=sys.stderr) return (dbus.ObjectPath(self.path), dbus.ByteArray(b""), dbus.ByteArray(raw_bytes), dbus.String("text/plain")) @dbus.service.method(SS_SESSION_IFACE, in_signature="", out_signature="") def Close(self): self.service.remove_session(self.session_id) self.remove_from_connection() class Item(dbus.service.Object): def __init__(self, bus, service, item_id, label, attributes, secret_bytes, created=None, modified=None): self.bus = bus self.service = service self.item_id = item_id self.label = label self.attributes = attributes self.secret_bytes = secret_bytes now = int(time.time()) self.created = created or now self.modified = modified or now self.path = f"{SS_PATH}/collection/login/{item_id}" super().__init__(bus, self.path) @dbus.service.method(SS_ITEM_IFACE, in_signature="", out_signature="o") def Delete(self): self.service.remove_item(self.item_id) self.remove_from_connection() return dbus.ObjectPath("/") @dbus.service.method(SS_ITEM_IFACE, in_signature="o", out_signature="(oayays)") def GetSecret(self, session_path): self.service.check_token_file_sync() session = self.service.get_session(str(session_path)) if session: return session.encrypt_secret(self.secret_bytes) return ( dbus.ObjectPath(session_path), dbus.ByteArray(b""), dbus.ByteArray(self.secret_bytes), dbus.String("text/plain"), ) @dbus.service.method(SS_ITEM_IFACE, in_signature="(oayays)", out_signature="") def SetSecret(self, secret_struct): session_path, _, _, _ = secret_struct session = self.service.get_session(str(session_path)) if session: self.secret_bytes = session.decrypt_secret(secret_struct) else: _, _, secret_bytes, _ = secret_struct self.secret_bytes = bytes(secret_bytes) self.modified = int(time.time()) self.service.save() @dbus.service.method(DBUS_PROP_IFACE, in_signature="ss", out_signature="v") def Get(self, interface_name, property_name): return self.GetAll(interface_name).get(property_name, "") @dbus.service.method(DBUS_PROP_IFACE, in_signature="s", out_signature="a{sv}") def GetAll(self, interface_name): if interface_name == SS_ITEM_IFACE: return { "Locked": dbus.Boolean(False), "Attributes": dbus.Dictionary(self.attributes, signature="ss"), "Label": dbus.String(self.label), "Created": dbus.UInt64(self.created), "Modified": dbus.UInt64(self.modified), } return {} @dbus.service.method(DBUS_PROP_IFACE, in_signature="ssv", out_signature="") def Set(self, interface_name, property_name, value): if interface_name == SS_ITEM_IFACE: if property_name == "Label": self.label = str(value) self.modified = int(time.time()) self.service.save() elif property_name == "Attributes": self.attributes = {str(k): str(v) for k, v in value.items()} self.modified = int(time.time()) self.service.save() class Collection(dbus.service.Object): def __init__(self, bus, service, path, label="Login"): self.bus = bus self.service = service self.path = path self.label = label super().__init__(bus, self.path) @dbus.service.method(SS_COLLECTION_IFACE, in_signature="", out_signature="o") def Delete(self): return dbus.ObjectPath("/") @dbus.service.method(SS_COLLECTION_IFACE, in_signature="a{ss}", out_signature="ao") def SearchItems(self, attributes): self.service.check_token_file_sync() results = [] for item in self.service.items.values(): match = True for k, v in attributes.items(): if item.attributes.get(str(k)) != str(v): match = False break if match: results.append(dbus.ObjectPath(item.path)) return results @dbus.service.method(SS_COLLECTION_IFACE, in_signature="a{sv}(oayays)b", out_signature="oo") def CreateItem(self, properties, secret_struct, replace): label = "" attrs = {} for k, v in properties.items(): if k.endswith(".Label") or k == "Label": label = str(v) elif k.endswith(".Attributes") or k == "Attributes": attrs = {str(attr_k): str(attr_v) for attr_k, attr_v in v.items()} session_path, _, _, _ = secret_struct session = self.service.get_session(str(session_path)) if session: secret_bytes = session.decrypt_secret(secret_struct) else: _, _, secret_b, _ = secret_struct secret_bytes = bytes(secret_b) existing_id = None if replace and attrs: for item_id, item in list(self.service.items.items()): if item.attributes == attrs: existing_id = item_id break if existing_id: item = self.service.items[existing_id] item.label = label or item.label item.secret_bytes = bytes(secret_bytes) item.modified = int(time.time()) else: item_id = "i" + uuid.uuid4().hex[:12] item = Item(self.bus, self.service, item_id, label, attrs, bytes(secret_bytes)) self.service.items[item_id] = item self.service.save() return (dbus.ObjectPath(item.path), dbus.ObjectPath("/")) @dbus.service.method(DBUS_PROP_IFACE, in_signature="ss", out_signature="v") def Get(self, interface_name, property_name): return self.GetAll(interface_name).get(property_name, "") @dbus.service.method(DBUS_PROP_IFACE, in_signature="s", out_signature="a{sv}") def GetAll(self, interface_name): if interface_name == SS_COLLECTION_IFACE: item_paths = [dbus.ObjectPath(item.path) for item in self.service.items.values()] return { "Items": dbus.Array(item_paths, signature="o"), "Label": dbus.String(self.label), "Locked": dbus.Boolean(False), "Created": dbus.UInt64(0), "Modified": dbus.UInt64(0), } return {} class SecretService(dbus.service.Object): def __init__(self, bus): self.bus = bus self.sessions = {} self.items = {} self.collections = {} super().__init__(bus, SS_PATH) data = load_data() items_data = data.get("collections", {}).get("login", {}).get("items", {}) for item_id, idata in items_data.items(): secret_bytes = idata.get("secret", "").encode("utf-8") item = Item( bus, self, item_id, idata.get("label", ""), idata.get("attributes", {}), secret_bytes, idata.get("created"), idata.get("modified"), ) self.items[item_id] = item # Register collections at all standard paths paths = [ (f"{SS_PATH}/collection/login", "Login"), (f"{SS_PATH}/collection/default", "Default"), (f"{SS_PATH}/aliases/default", "Default"), (f"{SS_PATH}/aliases/login", "Login"), ] for p, lbl in paths: self.collections[p] = Collection(bus, self, p, lbl) self.bus_name = dbus.service.BusName(BUS_NAME, bus) def check_token_file_sync(self): if os.path.exists(AGY_TOKEN_FILE): try: with open(AGY_TOKEN_FILE, "r") as f: content = f.read().strip() if content: found = False for item in self.items.values(): if item.attributes.get("service") == "antigravity" and item.attributes.get("username") == "token": if item.secret_bytes.decode("utf-8", errors="replace") != content: item.secret_bytes = content.encode("utf-8") item.modified = int(time.time()) found = True break if not found: item_id = "antigravity_oauth_token" item = Item( self.bus, self, item_id, "Antigravity CLI OAuth Token", {"service": "antigravity", "username": "token"}, content.encode("utf-8"), ) self.items[item_id] = item self.save() except Exception as e: print(f"Error checking token file sync: {e}", file=sys.stderr) def get_session(self, session_path): for session in self.sessions.values(): if session.path == session_path: return session return None def remove_session(self, session_id): if session_id in self.sessions: del self.sessions[session_id] def remove_item(self, item_id): if item_id in self.items: del self.items[item_id] self.save() def save(self): items_dict = {} for item_id, item in self.items.items(): try: secret_str = item.secret_bytes.decode("utf-8", errors="replace") except Exception: secret_str = "" items_dict[item_id] = { "label": item.label, "attributes": item.attributes, "secret": secret_str, "created": item.created, "modified": item.modified, } if item.attributes.get("service") == "antigravity" and item.attributes.get("username") == "token": sync_to_agy_token_file(secret_str) data = {"collections": {"login": {"label": "Login", "items": items_dict}}} save_data(data) @dbus.service.method(SS_SERVICE_IFACE, in_signature="sv", out_signature="vo") def OpenSession(self, algorithm, input_var): session_id = "s" + uuid.uuid4().hex[:12] client_key_bytes = None if algorithm == "dh-ietf1024-sha256-aes128-cbc-pkcs7": client_key_bytes = bytes(input_var) session = Session(self.bus, self, session_id, algorithm, client_key_bytes) self.sessions[session_id] = session if algorithm == "dh-ietf1024-sha256-aes128-cbc-pkcs7": return (dbus.ByteArray(session.server_public_key_bytes), dbus.ObjectPath(session.path)) return (dbus.String("", variant_level=1), dbus.ObjectPath(session.path)) @dbus.service.method(SS_SERVICE_IFACE, in_signature="a{sv}s", out_signature="oo") def CreateCollection(self, properties, alias): col_path = f"{SS_PATH}/collection/login" return (dbus.ObjectPath(col_path), dbus.ObjectPath("/")) @dbus.service.method(SS_SERVICE_IFACE, in_signature="a{ss}", out_signature="aoao") def SearchItems(self, attributes): self.check_token_file_sync() results = [] for item in self.items.values(): match = True for k, v in attributes.items(): if item.attributes.get(str(k)) != str(v): match = False break if match: results.append(dbus.ObjectPath(item.path)) return (dbus.Array(results, signature="o"), dbus.Array([], signature="o")) @dbus.service.method(SS_SERVICE_IFACE, in_signature="ao", out_signature="aoo") def Unlock(self, objects): return (objects, dbus.ObjectPath("/")) @dbus.service.method(SS_SERVICE_IFACE, in_signature="ao", out_signature="aoo") def Lock(self, objects): return (dbus.Array([], signature="o"), dbus.ObjectPath("/")) @dbus.service.method(SS_SERVICE_IFACE, in_signature="aoo", out_signature="a{o(oayays)}") def GetSecrets(self, item_paths, session_path): self.check_token_file_sync() results = {} session = self.get_session(str(session_path)) for item in self.items.values(): if dbus.ObjectPath(item.path) in item_paths: if session: results[dbus.ObjectPath(item.path)] = session.encrypt_secret(item.secret_bytes) else: results[dbus.ObjectPath(item.path)] = ( dbus.ObjectPath(session_path), dbus.ByteArray(b""), dbus.ByteArray(item.secret_bytes), dbus.String("text/plain"), ) return results @dbus.service.method(SS_SERVICE_IFACE, in_signature="s", out_signature="o") def ReadAlias(self, name): return dbus.ObjectPath(f"{SS_PATH}/collection/login") @dbus.service.method(SS_SERVICE_IFACE, in_signature="so", out_signature="") def SetAlias(self, name, collection_path): pass @dbus.service.method(DBUS_PROP_IFACE, in_signature="ss", out_signature="v") def Get(self, interface_name, property_name): return self.GetAll(interface_name).get(property_name, "") @dbus.service.method(DBUS_PROP_IFACE, in_signature="s", out_signature="a{sv}") def GetAll(self, interface_name): if interface_name == SS_SERVICE_IFACE: col_paths = [dbus.ObjectPath(f"{SS_PATH}/collection/login")] return {"Collections": dbus.Array(col_paths, signature="o")} return {} def main(): dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SessionBus() service = SecretService(bus) print("Secret Service Daemon started successfully.", flush=True) loop = GLib.MainLoop() try: loop.run() except KeyboardInterrupt: pass if __name__ == "__main__": main()