/* * Copyright (c) 2025 Amlogic, Inc. All rights reserved. * * This source code is subject to the terms and conditions defined in the * file 'LICENSE' which is part of this source code package. * * default_agent.c - Standalone BlueZ agent daemon * * Monitors org.bluez on the system D-Bus, registers itself as the default * agent (org.bluez.Agent1) and auto-confirms pairing requests. * * This agent is intended for embedded unattended devices (e.g. set-top * boxes) where there is no interactive user: pairing confirmations are * automatically accepted. Only enable it on trusted products via * USE_DEFAULT_AGENT=y. * * Passkey handling: * - default passkey is 0000 * - command line option -p sets a fixed passkey * - at runtime, typing "passkey " on stdin (e.g. serial console) * updates the passkey; "passkey 000000" restores the default 0000 * * Build: see Makefile (USE_DEFAULT_AGENT=y) * Depends only on gio-2.0/glib-2.0/dbus-1. */ #include #include #include #include #include #include #include #define AGENT_PATH "/org/bluez/agent" #define AGENT_INTERFACE "org.bluez.Agent1" #define BLUEZ_NAME "org.bluez" #define AGENT_MANAGER_PATH "/org/bluez" #define AGENT_MANAGER_IFACE "org.bluez.AgentManager1" #define DBUS_NAME "org.freedesktop.DBus" #define DBUS_PATH "/org/freedesktop/DBus" #define DBUS_IFACE "org.freedesktop.DBus" #define DEFAULT_PASSKEY "0000" #define DEFAULT_CAPABILITY "NoInputNoOutput" #define AGENT_PROMPT "default-agent: " #define PASSKEY_MAX_LEN 6 static const gchar *opt_capability = NULL; static gchar *opt_passkey = NULL; static gboolean opt_debug = FALSE; static gchar passkey[PASSKEY_MAX_LEN + 1]; static GDBusConnection *dbus_conn = NULL; static gboolean agent_registered = FALSE; static guint agent_reg_id = 0; static guint name_owner_sub_id = 0; static guint stdin_watch_id = 0; static GMainLoop *main_loop = NULL; static const GDBusInterfaceVTable agent_vtable; static const GDBusInterfaceInfo agent_interface_info; /* ------------------------------------------------------------------------- */ /* Logging */ /* ------------------------------------------------------------------------- */ #define info(fmt, ...) g_print(AGENT_PROMPT fmt "\n", ##__VA_ARGS__) #define err(fmt, ...) g_printerr(AGENT_PROMPT "ERROR: " fmt "\n", ##__VA_ARGS__) #define dbg(fmt, ...) \ G_STMT_START { \ if (opt_debug) \ g_print(AGENT_PROMPT "DEBUG: " fmt "\n", ##__VA_ARGS__); \ } \ G_STMT_END /* ------------------------------------------------------------------------- */ /* Passkey */ /* ------------------------------------------------------------------------- */ static gboolean validate_passkey(const gchar *str) { gsize len; if (!str) return FALSE; len = strlen(str); if (len == 0 || len > PASSKEY_MAX_LEN) return FALSE; if (strspn(str, "0123456789") != len) return FALSE; return TRUE; } static gboolean set_passkey_internal(const gchar *str, gboolean verbose) { gsize len; if (!validate_passkey(str)) { if (verbose) { info("invalid passkey '%s', use 1-%d digits", str ? str : "(null)", PASSKEY_MAX_LEN); } return FALSE; } len = strlen(str); memset(passkey, 0, sizeof(passkey)); memcpy(passkey, str, len); if (verbose) info("passkey set to %s", passkey); return TRUE; } static void set_passkey_from_command(const gchar *str) { gchar *tmp; if (!str) { info("usage: passkey <1-%d digits>", PASSKEY_MAX_LEN); return; } tmp = g_strdup(str); g_strstrip(tmp); if (!*tmp) { info("usage: passkey <1-%d digits>", PASSKEY_MAX_LEN); g_free(tmp); return; } set_passkey_internal(tmp, TRUE); g_free(tmp); } /* ------------------------------------------------------------------------- */ /* BlueZ Agent registration */ /* ------------------------------------------------------------------------- */ static gboolean unregister_agent(void) { GVariant *res; GError *error = NULL; if (!agent_registered || !dbus_conn) return TRUE; info("unregistering agent..."); res = g_dbus_connection_call_sync(dbus_conn, BLUEZ_NAME, AGENT_MANAGER_PATH, AGENT_MANAGER_IFACE, "UnregisterAgent", g_variant_new("(o)", AGENT_PATH), NULL, G_DBUS_CALL_FLAGS_NONE, 3000, NULL, &error); if (!res) { /* * BlueZ may already have disappeared. * In that case the registration is already gone. */ if (error) { dbg("UnregisterAgent failed: %s", error->message); g_error_free(error); } } else { g_variant_unref(res); } agent_registered = FALSE; info("agent unregistered"); return TRUE; } static gboolean register_agent(void) { GVariant *res; GError *error = NULL; if (!dbus_conn) return FALSE; if (agent_registered) { dbg("agent already registered"); return TRUE; } /* * Make sure BlueZ is currently available. */ res = g_dbus_connection_call_sync( dbus_conn, DBUS_NAME, DBUS_PATH, DBUS_IFACE, "GetNameOwner", g_variant_new("(s)", BLUEZ_NAME), G_VARIANT_TYPE("(s)"), G_DBUS_CALL_FLAGS_NONE, 3000, NULL, &error); if (!res) { dbg("BlueZ is not available"); if (error) g_error_free(error); return FALSE; } g_variant_unref(res); info("registering agent..."); /* * RegisterAgent(object, capability) */ res = g_dbus_connection_call_sync( dbus_conn, BLUEZ_NAME, AGENT_MANAGER_PATH, AGENT_MANAGER_IFACE, "RegisterAgent", g_variant_new("(os)", AGENT_PATH, opt_capability), NULL, G_DBUS_CALL_FLAGS_NONE, 3000, NULL, &error); if (!res) { if (error) { err("RegisterAgent failed: %s", error->message); g_error_free(error); } else { err("RegisterAgent failed"); } return FALSE; } g_variant_unref(res); /* * RegisterAgent succeeded. */ agent_registered = TRUE; /* * RequestDefaultAgent(object) */ res = g_dbus_connection_call_sync(dbus_conn, BLUEZ_NAME, AGENT_MANAGER_PATH, AGENT_MANAGER_IFACE, "RequestDefaultAgent", g_variant_new("(o)", AGENT_PATH), NULL, G_DBUS_CALL_FLAGS_NONE, 3000, NULL, &error); if (!res) { if (error) { err("RequestDefaultAgent failed: %s", error->message); g_error_free(error); } else { err("RequestDefaultAgent failed"); } /* * Roll back RegisterAgent. */ unregister_agent(); return FALSE; } g_variant_unref(res); info("agent registered as default " "(capability=%s, passkey=%s)", opt_capability, passkey); return TRUE; } /* ------------------------------------------------------------------------- */ /* BlueZ NameOwnerChanged */ /* ------------------------------------------------------------------------- */ static void on_name_owner_changed(GDBusConnection *conn, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { const gchar *name; const gchar *old_owner; const gchar *new_owner; (void)conn; (void)sender_name; (void)object_path; (void)interface_name; (void)signal_name; (void)user_data; g_variant_get(parameters, "(&s&s&s)", &name, &old_owner, &new_owner); if (g_strcmp0(name, BLUEZ_NAME) != 0) return; dbg("NameOwnerChanged %s: '%s' -> '%s'", name, old_owner, new_owner); /* * BlueZ disappeared. */ if (!new_owner || *new_owner == '\0') { if (agent_registered) { agent_registered = FALSE; info("bluez disappeared, agent registration lost"); } return; } /* * BlueZ appeared. */ if (!old_owner || *old_owner == '\0') { info("bluez appeared, registering agent..."); register_agent(); return; } /* * BlueZ owner changed. * * Usually means bluetoothd restarted. */ if (g_strcmp0(old_owner, new_owner) != 0) { agent_registered = FALSE; info("bluez owner changed, registering agent..."); register_agent(); return; } } /* ------------------------------------------------------------------------- */ /* D-Bus setup */ /* ------------------------------------------------------------------------- */ static gboolean setup_dbus(void) { GError *error = NULL; /* * Connect directly to system bus. * * We intentionally do NOT use g_bus_own_name(). */ dbus_conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (!dbus_conn) { err("failed to connect to system bus: %s", error ? error->message : "unknown error"); if (error) g_error_free(error); return FALSE; } info("connected to system bus"); /* * Export Agent1 object. */ agent_reg_id = g_dbus_connection_register_object( dbus_conn, AGENT_PATH, (GDBusInterfaceInfo *)&agent_interface_info, (GDBusInterfaceVTable *)&agent_vtable, NULL, NULL, &error); if (agent_reg_id == 0) { err("failed to register agent object: %s", error ? error->message : "unknown error"); if (error) g_error_free(error); return FALSE; } info("agent object exported at %s", AGENT_PATH); /* * Watch BlueZ NameOwnerChanged. * * This allows us to handle: * * 1. BlueZ already running * 2. BlueZ starts later * 3. BlueZ restarts */ name_owner_sub_id = g_dbus_connection_signal_subscribe( dbus_conn, DBUS_NAME, DBUS_IFACE, "NameOwnerChanged", DBUS_PATH, BLUEZ_NAME, G_DBUS_SIGNAL_FLAGS_NONE, on_name_owner_changed, NULL, NULL); if (name_owner_sub_id == 0) { err("failed to subscribe NameOwnerChanged"); return FALSE; } /* * Check whether BlueZ is already running. */ if (!register_agent()) { info("bluez is not running yet, waiting for BlueZ..."); } return TRUE; } /* ------------------------------------------------------------------------- */ /* Agent methods */ /* ------------------------------------------------------------------------- */ static void agent_release(void) { agent_registered = FALSE; info("agent released by bluez"); } static void handle_request_pincode(GDBusMethodInvocation *invocation) { info("RequestPinCode -> %s", passkey); g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", passkey)); } static void handle_request_passkey(GDBusMethodInvocation *invocation) { guint32 value = 0; value = (guint32)g_ascii_strtoull(passkey, NULL, 10); info("RequestPasskey -> %06u", value); g_dbus_method_invocation_return_value(invocation, g_variant_new("(u)", value)); } static void agent_method_call(GDBusConnection *conn, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { (void)conn; (void)sender; (void)object_path; (void)interface_name; (void)user_data; if (g_strcmp0(method_name, "Release") == 0) { agent_release(); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "RequestPinCode") == 0) { handle_request_pincode(invocation); } else if (g_strcmp0(method_name, "RequestPasskey") == 0) { handle_request_passkey(invocation); } else if (g_strcmp0(method_name, "DisplayPinCode") == 0) { const gchar *device; const gchar *pin; g_variant_get(parameters, "(&o&s)", &device, &pin); info("DisplayPinCode device=%s pin=%s", device, pin); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "DisplayPasskey") == 0) { const gchar *device; guint32 passkey_val; guint16 entered; g_variant_get(parameters, "(&ouq)", &device, &passkey_val, &entered); info("DisplayPasskey device=%s passkey=%06u entered=%u", device, passkey_val, entered); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "RequestConfirmation") == 0) { const gchar *device; guint32 passkey_val; g_variant_get(parameters, "(&ou)", &device, &passkey_val); /* * Embedded unattended device: * automatically accept confirmation. */ info("RequestConfirmation device=%s passkey=%06u -> auto accept", device, passkey_val); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "RequestAuthorization") == 0) { const gchar *device; g_variant_get(parameters, "(&o)", &device); info("RequestAuthorization device=%s -> auto accept", device); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "AuthorizeService") == 0) { const gchar *device; const gchar *uuid; g_variant_get(parameters, "(&o&s)", &device, &uuid); info("AuthorizeService device=%s uuid=%s -> auto accept", device, uuid); g_dbus_method_invocation_return_value(invocation, NULL); } else if (g_strcmp0(method_name, "Cancel") == 0) { info("Cancel"); g_dbus_method_invocation_return_value(invocation, NULL); } else { err("unknown Agent1 method: %s", method_name); g_dbus_method_invocation_return_dbus_error( invocation, "org.bluez.Error.InvalidArgs", "Unknown method"); } } /* ------------------------------------------------------------------------- */ /* Agent interface description */ /* ------------------------------------------------------------------------- */ static const GDBusInterfaceVTable agent_vtable = { .method_call = agent_method_call, .get_property = NULL, .set_property = NULL, }; /* * Common arguments. */ static const GDBusArgInfo arg_device = {-1, "device", "o", NULL}; static const GDBusArgInfo arg_pin = {-1, "pincode", "s", NULL}; static const GDBusArgInfo arg_passkey = {-1, "passkey", "u", NULL}; static const GDBusArgInfo arg_entered = {-1, "entered", "q", NULL}; static const GDBusArgInfo arg_uuid = {-1, "uuid", "s", NULL}; static const GDBusArgInfo arg_out_pin = {-1, "pincode", "s", NULL}; static const GDBusArgInfo arg_out_passkey = {-1, "passkey", "u", NULL}; /* * RequestPinCode(o) -> (s) */ static const GDBusArgInfo *req_pin_in[] = {&arg_device, NULL}; static const GDBusArgInfo *req_pin_out[] = {&arg_out_pin, NULL}; /* * RequestPasskey(o) -> (u) */ static const GDBusArgInfo *req_passkey_in[] = {&arg_device, NULL}; static const GDBusArgInfo *req_passkey_out[] = {&arg_out_passkey, NULL}; /* * DisplayPinCode(o, s) */ static const GDBusArgInfo *display_pin_in[] = {&arg_device, &arg_pin, NULL}; /* * DisplayPasskey(o, u, q) */ static const GDBusArgInfo *display_passkey_in[] = {&arg_device, &arg_passkey, &arg_entered, NULL}; /* * RequestConfirmation(o, u) */ static const GDBusArgInfo *confirm_in[] = {&arg_device, &arg_passkey, NULL}; /* * RequestAuthorization(o) */ static const GDBusArgInfo *authorize_in[] = {&arg_device, NULL}; /* * AuthorizeService(o, s) */ static const GDBusArgInfo *authorize_service_in[] = {&arg_device, &arg_uuid, NULL}; static const GDBusMethodInfo m_release = {-1, "Release", NULL, NULL, NULL}; static const GDBusMethodInfo m_req_pin = {-1, "RequestPinCode", (GDBusArgInfo **)req_pin_in, (GDBusArgInfo **)req_pin_out, NULL}; static const GDBusMethodInfo m_req_passkey = { -1, "RequestPasskey", (GDBusArgInfo **)req_passkey_in, (GDBusArgInfo **)req_passkey_out, NULL}; static const GDBusMethodInfo m_display_pin = { -1, "DisplayPinCode", (GDBusArgInfo **)display_pin_in, NULL, NULL}; static const GDBusMethodInfo m_display_passkey = { -1, "DisplayPasskey", (GDBusArgInfo **)display_passkey_in, NULL, NULL}; static const GDBusMethodInfo m_confirm = { -1, "RequestConfirmation", (GDBusArgInfo **)confirm_in, NULL, NULL}; static const GDBusMethodInfo m_authorize = { -1, "RequestAuthorization", (GDBusArgInfo **)authorize_in, NULL, NULL}; static const GDBusMethodInfo m_authorize_service = { -1, "AuthorizeService", (GDBusArgInfo **)authorize_service_in, NULL, NULL}; static const GDBusMethodInfo m_cancel = {-1, "Cancel", NULL, NULL, NULL}; static const GDBusMethodInfo *agent_methods[] = { &m_release, &m_req_pin, &m_req_passkey, &m_display_pin, &m_display_passkey, &m_confirm, &m_authorize, &m_authorize_service, &m_cancel, NULL}; static const GDBusInterfaceInfo agent_interface_info = { -1, AGENT_INTERFACE, (GDBusMethodInfo **)agent_methods, NULL, NULL, NULL, NULL}; /* ------------------------------------------------------------------------- */ /* stdin */ /* ------------------------------------------------------------------------- */ static gboolean on_stdin_input(GIOChannel *channel, GIOCondition cond, gpointer user_data) { gchar *line = NULL; gsize len = 0; GError *error = NULL; GIOStatus status; (void)user_data; if (cond & (G_IO_HUP | G_IO_ERR | G_IO_NVAL)) { stdin_watch_id = 0; info("stdin closed"); return G_SOURCE_REMOVE; } status = g_io_channel_read_line(channel, &line, &len, NULL, &error); if (status != G_IO_STATUS_NORMAL || !line) { if (error) { dbg("stdin read failed: %s", error->message); g_error_free(error); } g_free(line); if (status == G_IO_STATUS_EOF) { stdin_watch_id = 0; return G_SOURCE_REMOVE; } return G_SOURCE_CONTINUE; } g_strstrip(line); if (g_str_has_prefix(line, "passkey")) { const gchar *arg = line + strlen("passkey"); /* * Require whitespace or end-of-line. */ if (*arg == '\0') { info("usage: passkey <1-%d digits>", PASSKEY_MAX_LEN); } else if (g_ascii_isspace(*arg)) { set_passkey_from_command(arg); } else { info("unknown command, supported: passkey <1-%d digits>", PASSKEY_MAX_LEN); } } else if (line[0] != '\0') { info("unknown command, supported: passkey <1-%d digits>", PASSKEY_MAX_LEN); } g_free(line); return G_SOURCE_CONTINUE; } /* ------------------------------------------------------------------------- */ /* Signal handling */ /* ------------------------------------------------------------------------- */ static gboolean on_signal_exit(gpointer user_data) { GMainLoop *loop = user_data; info("shutting down"); /* * Unregister from BlueZ while the D-Bus connection is still alive. */ unregister_agent(); if (loop) g_main_loop_quit(loop); return G_SOURCE_REMOVE; } /* ------------------------------------------------------------------------- */ /* Cleanup */ /* ------------------------------------------------------------------------- */ static void cleanup(void) { /* * Remove stdin watch. */ if (stdin_watch_id != 0) { g_source_remove(stdin_watch_id); stdin_watch_id = 0; } /* * Remove BlueZ NameOwnerChanged subscription. */ if (dbus_conn && name_owner_sub_id != 0) { g_dbus_connection_signal_unsubscribe(dbus_conn, name_owner_sub_id); name_owner_sub_id = 0; } /* * Unregister Agent from BlueZ. */ unregister_agent(); /* * Unexport Agent1 object. */ if (dbus_conn && agent_reg_id != 0) { g_dbus_connection_unregister_object(dbus_conn, agent_reg_id); agent_reg_id = 0; } /* * Close our reference to system bus connection. */ if (dbus_conn) { g_object_unref(dbus_conn); dbus_conn = NULL; } } /* ------------------------------------------------------------------------- */ /* Options */ /* ------------------------------------------------------------------------- */ static const GOptionEntry options[] = { {"capability", 'c', 0, G_OPTION_ARG_STRING, &opt_capability, "Agent capability " "(DisplayOnly, DisplayYesNo, KeyboardOnly, " "NoInputNoOutput, KeyboardDisplay)", "CAPABILITY"}, {"passkey", 'p', 0, G_OPTION_ARG_STRING, &opt_passkey, "Fixed passkey for pairing (default 0000)", "PASSKEY"}, {"debug", 'd', 0, G_OPTION_ARG_NONE, &opt_debug, "Enable debug output", NULL}, {NULL}}; /* ------------------------------------------------------------------------- */ /* main */ /* ------------------------------------------------------------------------- */ int main(int argc, char *argv[]) { GOptionContext *context; GError *error = NULL; main_loop = g_main_loop_new(NULL, FALSE); /* * Initialize default passkey. */ g_strlcpy(passkey, DEFAULT_PASSKEY, sizeof(passkey)); /* * Parse command line. */ context = g_option_context_new("- standalone BlueZ auto-confirm agent"); g_option_context_add_main_entries(context, options, NULL); if (!g_option_context_parse(context, &argc, &argv, &error)) { err("option parsing failed: %s", error ? error->message : "unknown error"); if (error) g_error_free(error); g_option_context_free(context); g_main_loop_unref(main_loop); return 1; } if (!opt_capability) { opt_capability = g_strdup(DEFAULT_CAPABILITY); } /* * Validate capability. */ { static const gchar *valid_caps[] = {"DisplayOnly", "DisplayYesNo", "KeyboardOnly", "NoInputNoOutput", "KeyboardDisplay", NULL}; gboolean ok = FALSE; for (int i = 0; valid_caps[i]; i++) { if (g_strcmp0(opt_capability, valid_caps[i]) == 0) { ok = TRUE; break; } } if (!ok) { err("invalid capability '%s'", opt_capability); g_option_context_free(context); g_main_loop_unref(main_loop); return 1; } } /* * Validate command-line passkey. */ if (opt_passkey) { gchar *tmp = g_strdup(opt_passkey); g_strstrip(tmp); if (!set_passkey_internal(tmp, FALSE)) { err("invalid passkey '%s', using default %s", tmp, DEFAULT_PASSKEY); } g_free(tmp); } info("starting"); info("capability=%s", opt_capability); info("passkey=%s", passkey); /* * stdin command interface. * * Example: * * echo "passkey 123456" > /proc//fd/0 * * or type directly from a serial console. */ { GIOChannel *stdin_channel; stdin_channel = g_io_channel_unix_new(STDIN_FILENO); g_io_channel_set_close_on_unref(stdin_channel, FALSE); stdin_watch_id = g_io_add_watch(stdin_channel, G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL, on_stdin_input, NULL); g_io_channel_unref(stdin_channel); } /* * Handle SIGINT/SIGTERM. */ g_unix_signal_add(SIGINT, on_signal_exit, main_loop); g_unix_signal_add(SIGTERM, on_signal_exit, main_loop); /* * Connect system bus and setup Agent1. */ if (!setup_dbus()) { cleanup(); g_option_context_free(context); g_main_loop_unref(main_loop); return 1; } /* * Keep D-Bus connection and Agent object alive. */ info("entering main loop"); g_main_loop_run(main_loop); info("main loop exited"); /* * Cleanup. */ cleanup(); g_free(opt_passkey); opt_passkey = NULL; g_free(opt_capability); opt_capability = NULL; g_option_context_free(context); g_main_loop_unref(main_loop); main_loop = NULL; return 0; }