/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * Copyright (C) 2011 - 2015 Red Hat, Inc. * Copyright (C) 2011 Giovanni Campagna */ /** * SECTION:nm-secret-agent-simple * @short_description: A simple secret agent for NetworkManager * * #NMSecretAgentSimple is the secret agent used by nmtui-connect and nmcli. * * This is a stripped-down version of gnome-shell's ShellNetworkAgent, * with bits of the corresponding JavaScript code squished down into * it. It is intended to eventually be generic enough that it could * replace ShellNetworkAgent. */ #include "libnm-client-aux-extern/nm-default-client.h" #include "nm-secret-agent-simple.h" #include #include #include "nm-vpn-service-plugin.h" #include "nm-vpn-helpers.h" #include "libnm-glib-aux/nm-secret-utils.h" /*****************************************************************************/ typedef struct { char *request_id; NMSecretAgentSimple *self; NMConnection *connection; const char *setting_name; char **hints; NMSecretAgentOldGetSecretsFunc callback; gpointer callback_data; GCancellable *cancellable; NMSecretAgentGetSecretsFlags flags; } RequestData; enum { REQUEST_SECRETS, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = {0}; typedef struct { GHashTable *requests; char *path; gboolean enabled; } NMSecretAgentSimplePrivate; struct _NMSecretAgentSimple { NMSecretAgentOld parent; NMSecretAgentSimplePrivate _priv; }; struct _NMSecretAgentSimpleClass { NMSecretAgentOldClass parent; }; G_DEFINE_TYPE(NMSecretAgentSimple, nm_secret_agent_simple, NM_TYPE_SECRET_AGENT_OLD) #define NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self) \ _NM_GET_PRIVATE(self, NMSecretAgentSimple, NM_IS_SECRET_AGENT_SIMPLE, NMSecretAgentOld) /*****************************************************************************/ static void _request_data_free(gpointer data) { RequestData *request = data; g_free(request->request_id); nm_clear_g_cancellable(&request->cancellable); g_object_unref(request->connection); g_strfreev(request->hints); g_slice_free(RequestData, request); } static void _request_data_complete(RequestData *request, GVariant *secrets, GError *error, GHashTableIter *iter_to_remove) { NMSecretAgentSimple *self = request->self; NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); nm_assert((secrets != NULL) != (error != NULL)); request->callback(NM_SECRET_AGENT_OLD(request->self), request->connection, secrets, error, request->callback_data); if (iter_to_remove) g_hash_table_iter_remove(iter_to_remove); else g_hash_table_remove(priv->requests, request); } /*****************************************************************************/ /** * NMSecretAgentSimpleSecret: * @name: the user-visible name of the secret. Eg, "WEP Passphrase". * @value: the value of the secret * @password: %TRUE if this secret represents a password, %FALSE * if it represents non-secret data. * * A single "secret" being requested. */ typedef struct { NMSecretAgentSimpleSecret base; NMSetting *setting; char *property; } SecretReal; static void _secret_real_free(NMSecretAgentSimpleSecret *secret) { SecretReal *real = (SecretReal *) secret; g_free((char *) secret->pretty_name); g_free((char *) secret->entry_id); nm_free_secret(secret->value); g_free((char *) secret->vpn_type); g_free(real->property); g_clear_object(&real->setting); g_slice_free(SecretReal, real); } static NMSecretAgentSimpleSecret * _secret_real_new_plain(NMSecretAgentSecretType secret_type, const char *pretty_name, NMSetting *setting, const char *property) { SecretReal *real; gs_free char *value = NULL; nm_assert(property); nm_assert(NM_IS_SETTING(setting)); nm_assert(NM_IN_SET(secret_type, NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, NM_SECRET_AGENT_SECRET_TYPE_SECRET)); nm_assert(g_object_class_find_property(G_OBJECT_GET_CLASS(setting), property)); nm_assert((secret_type == NM_SECRET_AGENT_SECRET_TYPE_SECRET) == nm_setting_get_secret_flags(setting, property, NULL, NULL)); g_object_get(setting, property, &value, NULL); real = g_slice_new(SecretReal); *real = (SecretReal){ .base.secret_type = secret_type, .base.pretty_name = g_strdup(pretty_name), .base.entry_id = g_strdup_printf("%s.%s", nm_setting_get_name(setting), property), .base.value = g_steal_pointer(&value), .base.is_secret = (secret_type != NM_SECRET_AGENT_SECRET_TYPE_PROPERTY), .setting = g_object_ref(setting), .property = g_strdup(property), }; return &real->base; } static NMSecretAgentSimpleSecret * _secret_real_new_vpn_secret(const char *pretty_name, NMSetting *setting, const char *property, const char *vpn_type) { SecretReal *real; const char *value; nm_assert(property); nm_assert(NM_IS_SETTING_VPN(setting)); nm_assert(vpn_type); value = nm_setting_vpn_get_secret(NM_SETTING_VPN(setting), property); real = g_slice_new(SecretReal); *real = (SecretReal){ .base.secret_type = NM_SECRET_AGENT_SECRET_TYPE_VPN_SECRET, .base.pretty_name = g_strdup(pretty_name), .base.entry_id = g_strdup_printf("%s%s", NM_SECRET_AGENT_ENTRY_ID_PREFX_VPN_SECRETS, property), .base.value = g_strdup(value), .base.is_secret = TRUE, .base.vpn_type = g_strdup(vpn_type), .setting = g_object_ref(setting), .property = g_strdup(property), }; return &real->base; } static NMSecretAgentSimpleSecret * _secret_real_new_wireguard_peer_psk(NMSettingWireGuard *s_wg, const char *public_key, const char *preshared_key) { SecretReal *real; nm_assert(NM_IS_SETTING_WIREGUARD(s_wg)); nm_assert(public_key); real = g_slice_new(SecretReal); *real = (SecretReal){ .base.secret_type = NM_SECRET_AGENT_SECRET_TYPE_WIREGUARD_PEER_PSK, .base.pretty_name = g_strdup_printf(_("Preshared-key for %s"), public_key), .base.entry_id = g_strdup_printf(NM_SETTING_WIREGUARD_SETTING_NAME "." NM_SETTING_WIREGUARD_PEERS ".%s." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, public_key), .base.value = g_strdup(preshared_key), .base.is_secret = TRUE, .base.no_prompt_entry_id = TRUE, .setting = NM_SETTING(g_object_ref(s_wg)), .property = g_strdup(public_key), }; return &real->base; } /*****************************************************************************/ static gboolean add_8021x_secrets(RequestData *request, GPtrArray *secrets) { NMSetting8021x *s_8021x = nm_connection_get_setting_802_1x(request->connection); const char *eap_method; NMSecretAgentSimpleSecret *secret; /* If hints are given, then always ask for what the hints require */ if (request->hints && request->hints[0]) { char **iter; for (iter = request->hints; *iter; iter++) { secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _(*iter), NM_SETTING(s_8021x), *iter); g_ptr_array_add(secrets, secret); } return TRUE; } eap_method = nm_setting_802_1x_get_eap_method(s_8021x, 0); if (!eap_method) return FALSE; if (NM_IN_STRSET(eap_method, "md5", "leap", "ttls", "peap")) { /* TTLS and PEAP are actually much more complicated, but this complication * is not visible here since we only care about phase2 authentication * (and don't even care of which one) */ secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, _("Username"), NM_SETTING(s_8021x), NM_SETTING_802_1X_IDENTITY); g_ptr_array_add(secrets, secret); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Password"), NM_SETTING(s_8021x), NM_SETTING_802_1X_PASSWORD); g_ptr_array_add(secrets, secret); return TRUE; } if (nm_streq(eap_method, "tls")) { secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, _("Identity"), NM_SETTING(s_8021x), NM_SETTING_802_1X_IDENTITY); g_ptr_array_add(secrets, secret); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Private key password"), NM_SETTING(s_8021x), NM_SETTING_802_1X_PRIVATE_KEY_PASSWORD); g_ptr_array_add(secrets, secret); return TRUE; } return FALSE; } static gboolean add_wireless_secrets(RequestData *request, GPtrArray *secrets) { NMSettingWirelessSecurity *s_wsec = nm_connection_get_setting_wireless_security(request->connection); const char *key_mgmt = nm_setting_wireless_security_get_key_mgmt(s_wsec); NMSecretAgentSimpleSecret *secret; if (!key_mgmt || nm_streq(key_mgmt, "owe")) return FALSE; if (NM_IN_STRSET(key_mgmt, "wpa-psk", "sae")) { secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Password"), NM_SETTING(s_wsec), NM_SETTING_WIRELESS_SECURITY_PSK); g_ptr_array_add(secrets, secret); return TRUE; } if (nm_streq(key_mgmt, "none")) { guint32 index; char key[100]; index = nm_setting_wireless_security_get_wep_tx_keyidx(s_wsec); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Key"), NM_SETTING(s_wsec), nm_sprintf_buf(key, "wep-key%u", (guint) index)); g_ptr_array_add(secrets, secret); return TRUE; } if (nm_streq(key_mgmt, "iee8021x")) { if (nm_streq0(nm_setting_wireless_security_get_auth_alg(s_wsec), "leap")) { secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Password"), NM_SETTING(s_wsec), NM_SETTING_WIRELESS_SECURITY_LEAP_PASSWORD); g_ptr_array_add(secrets, secret); return TRUE; } else return add_8021x_secrets(request, secrets); } if (nm_streq(key_mgmt, "wpa-eap") || nm_streq(key_mgmt, "wpa-eap-suite-b-192")) return add_8021x_secrets(request, secrets); return FALSE; } static gboolean add_pppoe_secrets(RequestData *request, GPtrArray *secrets) { NMSettingPppoe *s_pppoe = nm_connection_get_setting_pppoe(request->connection); NMSecretAgentSimpleSecret *secret; secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, _("Username"), NM_SETTING(s_pppoe), NM_SETTING_PPPOE_USERNAME); g_ptr_array_add(secrets, secret); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_PROPERTY, _("Service"), NM_SETTING(s_pppoe), NM_SETTING_PPPOE_SERVICE); g_ptr_array_add(secrets, secret); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Password"), NM_SETTING(s_pppoe), NM_SETTING_PPPOE_PASSWORD); g_ptr_array_add(secrets, secret); return TRUE; } static NMSettingSecretFlags get_vpn_secret_flags(NMSettingVpn *s_vpn, const char *secret_name) { NMSettingSecretFlags flags = NM_SETTING_SECRET_FLAG_NONE; GHashTable *vpn_data; g_object_get(s_vpn, NM_SETTING_VPN_DATA, &vpn_data, NULL); nm_vpn_service_plugin_get_secret_flags(vpn_data, secret_name, &flags); g_hash_table_unref(vpn_data); return flags; } static void add_vpn_secret_helper(GPtrArray *secrets, NMSettingVpn *s_vpn, const char *name, const char *ui_name) { NMSecretAgentSimpleSecret *secret; NMSettingSecretFlags flags; int i; flags = get_vpn_secret_flags(s_vpn, name); if (flags & NM_SETTING_SECRET_FLAG_AGENT_OWNED || flags & NM_SETTING_SECRET_FLAG_NOT_SAVED) { secret = _secret_real_new_vpn_secret(ui_name, NM_SETTING(s_vpn), name, nm_setting_vpn_get_service_type(s_vpn)); /* Check for duplicates */ for (i = 0; i < secrets->len; i++) { NMSecretAgentSimpleSecret *s = secrets->pdata[i]; if (s->secret_type == secret->secret_type && nm_streq0(s->vpn_type, secret->vpn_type) && nm_streq0(s->entry_id, secret->entry_id)) { _secret_real_free(secret); return; } } g_ptr_array_add(secrets, secret); } } #define VPN_MSG_TAG "x-vpn-message:" static gboolean add_vpn_secrets(RequestData *request, GPtrArray *secrets, char **msg) { NMSettingVpn *s_vpn = nm_connection_get_setting_vpn(request->connection); const NmcVpnPasswordName *p; const char *vpn_msg = NULL; char **iter; /* If hints are given, then always ask for what the hints require */ if (request->hints) { for (iter = request->hints; *iter; iter++) { if (!vpn_msg && g_str_has_prefix(*iter, VPN_MSG_TAG)) vpn_msg = &(*iter)[NM_STRLEN(VPN_MSG_TAG)]; else add_vpn_secret_helper(secrets, s_vpn, *iter, *iter); } } NM_SET_OUT(msg, g_strdup(vpn_msg)); /* Now add what client thinks might be required, because hints may be empty or incomplete */ p = nm_vpn_get_secret_names(nm_setting_vpn_get_service_type(s_vpn)); while (p && p->name) { add_vpn_secret_helper(secrets, s_vpn, p->name, _(p->ui_name)); p++; } return TRUE; } static gboolean add_wireguard_secrets(RequestData *request, GPtrArray *secrets, char **msg, GError **error) { NMSettingWireGuard *s_wg; NMSecretAgentSimpleSecret *secret; guint i; s_wg = NM_SETTING_WIREGUARD( nm_connection_get_setting(request->connection, NM_TYPE_SETTING_WIREGUARD)); if (!s_wg) { g_set_error(error, NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_FAILED, "Cannot service a WireGuard secrets request %s for a connection without " "WireGuard settings", request->request_id); return FALSE; } if (!request->hints || !request->hints[0] || g_strv_contains(NM_CAST_STRV_CC(request->hints), NM_SETTING_WIREGUARD_PRIVATE_KEY)) { secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("WireGuard private-key"), NM_SETTING(s_wg), NM_SETTING_WIREGUARD_PRIVATE_KEY); g_ptr_array_add(secrets, secret); } if (request->hints) { for (i = 0; request->hints[i]; i++) { NMWireGuardPeer *peer; const char *name = request->hints[i]; gs_free char *public_key = NULL; if (nm_streq(name, NM_SETTING_WIREGUARD_PRIVATE_KEY)) continue; if (NM_STR_HAS_PREFIX(name, NM_SETTING_WIREGUARD_PEERS ".")) { const char *tmp; tmp = &name[NM_STRLEN(NM_SETTING_WIREGUARD_PEERS ".")]; if (NM_STR_HAS_SUFFIX(tmp, "." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY)) { public_key = g_strndup( tmp, strlen(tmp) - NM_STRLEN("." NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY)); } } if (!public_key) continue; peer = nm_setting_wireguard_get_peer_by_public_key(s_wg, public_key, NULL); g_ptr_array_add(secrets, _secret_real_new_wireguard_peer_psk( s_wg, (peer ? nm_wireguard_peer_get_public_key(peer) : public_key), (peer ? nm_wireguard_peer_get_preshared_key(peer) : NULL))); } } *msg = g_strdup_printf(_("Secrets are required to connect WireGuard VPN '%s'"), nm_connection_get_id(request->connection)); return TRUE; } typedef struct { GPid auth_dialog_pid; GString *auth_dialog_response; RequestData *request; GPtrArray *secrets; GCancellable *cancellable; gulong cancellable_id; guint child_watch_id; GInputStream *input_stream; GOutputStream *output_stream; char read_buf[5]; } AuthDialogData; static void _auth_dialog_data_free(AuthDialogData *data) { nm_clear_g_signal_handler(data->cancellable, &data->cancellable_id); g_clear_object(&data->cancellable); nm_clear_g_source(&data->child_watch_id); g_ptr_array_unref(data->secrets); g_spawn_close_pid(data->auth_dialog_pid); g_string_free(data->auth_dialog_response, TRUE); g_object_unref(data->input_stream); g_object_unref(data->output_stream); g_slice_free(AuthDialogData, data); } static void _auth_dialog_exited(GPid pid, int status, gpointer user_data) { AuthDialogData *data = user_data; RequestData *request = data->request; GPtrArray *secrets = data->secrets; NMSettingVpn *s_vpn = nm_connection_get_setting_vpn(request->connection); nm_auto_unref_keyfile GKeyFile *keyfile = NULL; gs_strfreev char **groups = NULL; gs_free char *title = NULL; gs_free char *message = NULL; int i; gs_free_error GError *error = NULL; data->child_watch_id = 0; nm_clear_g_cancellable_disconnect(data->cancellable, &data->cancellable_id); if (status != 0) { g_set_error(&error, NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_FAILED, "Auth dialog failed with error code %d\n", status); goto out; } keyfile = g_key_file_new(); if (!g_key_file_load_from_data(keyfile, data->auth_dialog_response->str, data->auth_dialog_response->len, G_KEY_FILE_NONE, &error)) { goto out; } groups = g_key_file_get_groups(keyfile, NULL); if (!nm_streq0(groups[0], "VPN Plugin UI")) { g_set_error(&error, NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_FAILED, "Expected [VPN Plugin UI] in auth dialog response"); goto out; } title = g_key_file_get_string(keyfile, "VPN Plugin UI", "Title", &error); if (!title) goto out; message = g_key_file_get_string(keyfile, "VPN Plugin UI", "Description", &error); if (!message) goto out; for (i = 1; groups[i]; i++) { gs_free char *pretty_name = NULL; if (!g_key_file_get_boolean(keyfile, groups[i], "IsSecret", NULL)) continue; if (!g_key_file_get_boolean(keyfile, groups[i], "ShouldAsk", NULL)) continue; pretty_name = g_key_file_get_string(keyfile, groups[i], "Label", NULL); g_ptr_array_add(secrets, _secret_real_new_vpn_secret(pretty_name, NM_SETTING(s_vpn), groups[i], nm_setting_vpn_get_service_type(s_vpn))); } out: /* Try to fall back to the hardwired VPN support if the auth dialog fails. * We may eventually get rid of the whole hardwired secrets handling at some point, * when the auth helpers are goode enough.. */ if (error && add_vpn_secrets(request, secrets, &message)) { g_clear_error(&error); if (!message) { message = g_strdup_printf(_("A password is required to connect to '%s'."), nm_connection_get_id(request->connection)); } } if (error) _request_data_complete(request, NULL, error, NULL); else { g_signal_emit(request->self, signals[REQUEST_SECRETS], 0, request->request_id, title, message, secrets); } _auth_dialog_data_free(data); } static void _request_cancelled(GObject *object, gpointer user_data) { _auth_dialog_data_free(user_data); } static void _auth_dialog_read_done(GObject *source_object, GAsyncResult *res, gpointer user_data) { GInputStream *auth_dialog_out = G_INPUT_STREAM(source_object); AuthDialogData *data = user_data; gssize read_size; gs_free_error GError *error = NULL; read_size = g_input_stream_read_finish(auth_dialog_out, res, &error); switch (read_size) { case -1: if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) _request_data_complete(data->request, NULL, error, NULL); _auth_dialog_data_free(data); break; case 0: /* Done reading. Let's wait for the auth dialog to exit so that we're able to collect the status. * Remember we can be cancelled in between. */ data->child_watch_id = g_child_watch_add(data->auth_dialog_pid, _auth_dialog_exited, data); data->cancellable = g_object_ref(data->request->cancellable); data->cancellable_id = g_cancellable_connect(data->cancellable, G_CALLBACK(_request_cancelled), data, NULL); break; default: g_string_append_len(data->auth_dialog_response, data->read_buf, read_size); g_input_stream_read_async(auth_dialog_out, data->read_buf, sizeof(data->read_buf), G_PRIORITY_DEFAULT, NULL, _auth_dialog_read_done, data); return; } g_input_stream_close(auth_dialog_out, NULL, NULL); } static void _auth_dialog_write_done(GObject *source_object, GAsyncResult *res, gpointer user_data) { GOutputStream *auth_dialog_out = G_OUTPUT_STREAM(source_object); _nm_unused gs_free char *auth_dialog_request_free = user_data; /* We don't care about write errors. If there are any problems, the * reader shall notice. */ g_output_stream_write_finish(auth_dialog_out, res, NULL); g_output_stream_close(auth_dialog_out, NULL, NULL); } static void _add_to_string(GString *string, const char *key, const char *value) { gs_strfreev char **lines = NULL; int i; lines = g_strsplit(value, "\n", -1); g_string_append(string, key); for (i = 0; lines[i]; i++) { g_string_append_c(string, '='); g_string_append(string, lines[i]); g_string_append_c(string, '\n'); } } static void _add_data_item_to_string(const char *key, const char *value, gpointer user_data) { GString *string = user_data; _add_to_string(string, "DATA_KEY", key); _add_to_string(string, "DATA_VAL", value); g_string_append_c(string, '\n'); } static void _add_secret_to_string(const char *key, const char *value, gpointer user_data) { GString *string = user_data; _add_to_string(string, "SECRET_KEY", key); _add_to_string(string, "SECRET_VAL", value); g_string_append_c(string, '\n'); } static gboolean try_spawn_vpn_auth_helper(RequestData *request, GPtrArray *secrets) { NMSettingVpn *s_vpn = nm_connection_get_setting_vpn(request->connection); gs_unref_ptrarray GPtrArray *auth_dialog_argv = NULL; NMVpnPluginInfo *plugin_info; const char *s; GPid auth_dialog_pid; int auth_dialog_in_fd; int auth_dialog_out_fd; GOutputStream *auth_dialog_in; GInputStream *auth_dialog_out; GError *error = NULL; GString *auth_dialog_request; gs_free char *auth_dialog_request_str = NULL; gsize auth_dialog_request_len; AuthDialogData *data; int i; plugin_info = nm_vpn_plugin_info_list_find_by_service(nm_vpn_get_plugin_infos(), nm_setting_vpn_get_service_type(s_vpn)); if (!plugin_info) return FALSE; s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "supports-external-ui-mode"); if (!_nm_utils_ascii_str_to_bool(s, FALSE)) return FALSE; auth_dialog_argv = g_ptr_array_new(); s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "auth-dialog"); g_return_val_if_fail(s, FALSE); g_ptr_array_add(auth_dialog_argv, (gpointer) s); g_ptr_array_add(auth_dialog_argv, "-u"); g_ptr_array_add(auth_dialog_argv, (gpointer) nm_connection_get_uuid(request->connection)); g_ptr_array_add(auth_dialog_argv, "-n"); g_ptr_array_add(auth_dialog_argv, (gpointer) nm_connection_get_id(request->connection)); g_ptr_array_add(auth_dialog_argv, "-s"); g_ptr_array_add(auth_dialog_argv, (gpointer) nm_setting_vpn_get_service_type(s_vpn)); g_ptr_array_add(auth_dialog_argv, "--external-ui-mode"); g_ptr_array_add(auth_dialog_argv, "-i"); if (request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW) g_ptr_array_add(auth_dialog_argv, "-r"); s = nm_vpn_plugin_info_lookup_property(plugin_info, "GNOME", "supports-hints"); if (_nm_utils_ascii_str_to_bool(s, FALSE)) { for (i = 0; request->hints[i]; i++) { g_ptr_array_add(auth_dialog_argv, "-t"); g_ptr_array_add(auth_dialog_argv, request->hints[i]); } } g_ptr_array_add(auth_dialog_argv, NULL); if (!g_spawn_async_with_pipes(NULL, (char **) auth_dialog_argv->pdata, NULL, G_SPAWN_CLOEXEC_PIPES | G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, &auth_dialog_pid, &auth_dialog_in_fd, &auth_dialog_out_fd, NULL, &error)) { return FALSE; } auth_dialog_in = g_unix_output_stream_new(auth_dialog_in_fd, TRUE); auth_dialog_out = g_unix_input_stream_new(auth_dialog_out_fd, TRUE); auth_dialog_request = g_string_new_len(NULL, 1024); nm_setting_vpn_foreach_data_item(s_vpn, _add_data_item_to_string, auth_dialog_request); nm_setting_vpn_foreach_secret(s_vpn, _add_secret_to_string, auth_dialog_request); g_string_append(auth_dialog_request, "DONE\nQUIT\n"); auth_dialog_request_len = auth_dialog_request->len; auth_dialog_request_str = g_string_free(auth_dialog_request, FALSE); data = g_slice_new(AuthDialogData); *data = (AuthDialogData){ .auth_dialog_response = g_string_new_len(NULL, sizeof(data->read_buf)), .auth_dialog_pid = auth_dialog_pid, .request = request, .secrets = g_ptr_array_ref(secrets), .input_stream = auth_dialog_out, .output_stream = auth_dialog_in, }; g_output_stream_write_async(auth_dialog_in, auth_dialog_request_str, auth_dialog_request_len, G_PRIORITY_DEFAULT, request->cancellable, _auth_dialog_write_done, auth_dialog_request_str); /* Ownership of the pointer was passed on to g_output_stream_write_async(). */ g_steal_pointer(&auth_dialog_request_str); g_input_stream_read_async(auth_dialog_out, data->read_buf, sizeof(data->read_buf), G_PRIORITY_DEFAULT, request->cancellable, _auth_dialog_read_done, data); return TRUE; } static void request_secrets_from_ui(RequestData *request) { gs_unref_ptrarray GPtrArray *secrets = NULL; gs_free_error GError *error = NULL; NMSecretAgentSimplePrivate *priv; NMSecretAgentSimpleSecret *secret; const char *title; gs_free char *msg = NULL; priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(request->self); g_return_if_fail(priv->enabled); /* We only handle requests for connection with @path if set. */ if (priv->path && !g_str_has_prefix(request->request_id, priv->path)) { g_set_error(&error, NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_FAILED, "Request for %s secrets doesn't match path %s", request->request_id, priv->path); goto out_fail_error; } secrets = g_ptr_array_new_with_free_func((GDestroyNotify) _secret_real_free); if (nm_connection_is_type(request->connection, NM_SETTING_WIRELESS_SETTING_NAME)) { NMSettingWireless *s_wireless; GBytes *ssid; gs_free char *ssid_utf8 = NULL; s_wireless = nm_connection_get_setting_wireless(request->connection); ssid = nm_setting_wireless_get_ssid(s_wireless); ssid_utf8 = nm_utils_ssid_to_utf8(g_bytes_get_data(ssid, NULL), g_bytes_get_size(ssid)); title = _("Authentication required by wireless network"); msg = g_strdup_printf( _("Passwords or encryption keys are required to access the wireless network '%s'."), ssid_utf8); if (!add_wireless_secrets(request, secrets)) goto out_fail; } else if (nm_connection_is_type(request->connection, NM_SETTING_WIRED_SETTING_NAME)) { title = _("Wired 802.1X authentication"); msg = g_strdup_printf(_("Secrets are required to access the wired network '%s'"), nm_connection_get_id(request->connection)); if (!add_8021x_secrets(request, secrets)) goto out_fail; } else if (nm_connection_is_type(request->connection, NM_SETTING_PPPOE_SETTING_NAME)) { title = _("DSL authentication"); msg = g_strdup_printf(_("Secrets are required for the DSL connection '%s'"), nm_connection_get_id(request->connection)); if (!add_pppoe_secrets(request, secrets)) goto out_fail; } else if (nm_connection_is_type(request->connection, NM_SETTING_GSM_SETTING_NAME)) { NMSettingGsm *s_gsm = nm_connection_get_setting_gsm(request->connection); if (g_strv_contains(NM_CAST_STRV_CC(request->hints), NM_SETTING_GSM_PIN)) { title = _("PIN code required"); msg = g_strdup(_("PIN code is needed for the mobile broadband device")); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("PIN"), NM_SETTING(s_gsm), NM_SETTING_GSM_PIN); g_ptr_array_add(secrets, secret); } else { title = _("Mobile broadband network password"); msg = g_strdup_printf(_("A password is required to connect to '%s'."), nm_connection_get_id(request->connection)); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Password"), NM_SETTING(s_gsm), NM_SETTING_GSM_PASSWORD); g_ptr_array_add(secrets, secret); } } else if (nm_connection_is_type(request->connection, NM_SETTING_MACSEC_SETTING_NAME)) { NMSettingMacsec *s_macsec = nm_connection_get_setting_macsec(request->connection); msg = g_strdup_printf(_("Secrets are required to access the MACsec network '%s'"), nm_connection_get_id(request->connection)); if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_PSK) { title = _("MACsec PSK authentication"); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("MKA CAK"), NM_SETTING(s_macsec), NM_SETTING_MACSEC_MKA_CAK); g_ptr_array_add(secrets, secret); } else { title = _("MACsec EAP authentication"); if (!add_8021x_secrets(request, secrets)) goto out_fail; } } else if (nm_connection_is_type(request->connection, NM_SETTING_WIREGUARD_SETTING_NAME)) { title = _("WireGuard VPN secret"); if (!add_wireguard_secrets(request, secrets, &msg, &error)) goto out_fail_error; } else if (nm_connection_is_type(request->connection, NM_SETTING_CDMA_SETTING_NAME)) { NMSettingCdma *s_cdma = nm_connection_get_setting_cdma(request->connection); title = _("Mobile broadband network password"); msg = g_strdup_printf(_("A password is required to connect to '%s'."), nm_connection_get_id(request->connection)); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Password"), NM_SETTING(s_cdma), NM_SETTING_CDMA_PASSWORD); g_ptr_array_add(secrets, secret); } else if (nm_connection_is_type(request->connection, NM_SETTING_BLUETOOTH_SETTING_NAME)) { NMSetting *setting = NULL; setting = nm_connection_get_setting_by_name(request->connection, NM_SETTING_BLUETOOTH_SETTING_NAME); if (setting && !nm_streq0(nm_setting_bluetooth_get_connection_type(NM_SETTING_BLUETOOTH(setting)), NM_SETTING_BLUETOOTH_TYPE_NAP)) { setting = nm_connection_get_setting_by_name(request->connection, NM_SETTING_GSM_SETTING_NAME); if (!setting) setting = nm_connection_get_setting_by_name(request->connection, NM_SETTING_CDMA_SETTING_NAME); } if (!setting) goto out_fail; title = _("Mobile broadband network password"); msg = g_strdup_printf(_("A password is required to connect to '%s'."), nm_connection_get_id(request->connection)); secret = _secret_real_new_plain(NM_SECRET_AGENT_SECRET_TYPE_SECRET, _("Password"), setting, "password"); g_ptr_array_add(secrets, secret); } else if (nm_connection_is_type(request->connection, NM_SETTING_VPN_SETTING_NAME)) { title = _("VPN password required"); if (try_spawn_vpn_auth_helper(request, secrets)) { /* This will emit REQUEST_SECRETS when ready */ return; } if (!add_vpn_secrets(request, secrets, &msg)) goto out_fail; if (!msg) { msg = g_strdup_printf(_("A password is required to connect to '%s'."), nm_connection_get_id(request->connection)); } } else goto out_fail; if (secrets->len == 0) goto out_fail; g_signal_emit(request->self, signals[REQUEST_SECRETS], 0, request->request_id, title, msg, secrets); return; out_fail: g_set_error(&error, NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_FAILED, "Cannot service a secrets request %s for a %s connection", request->request_id, nm_connection_get_connection_type(request->connection)); out_fail_error: _request_data_complete(request, NULL, error, NULL); } static void get_secrets(NMSecretAgentOld *agent, NMConnection *connection, const char *connection_path, const char *setting_name, const char **hints, NMSecretAgentGetSecretsFlags flags, NMSecretAgentOldGetSecretsFunc callback, gpointer callback_data) { NMSecretAgentSimple *self = NM_SECRET_AGENT_SIMPLE(agent); NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); RequestData *request; gs_free_error GError *error = NULL; gs_free char *request_id = NULL; const char *request_id_setting_name; request_id = g_strdup_printf("%s/%s", connection_path, setting_name); if (g_hash_table_contains(priv->requests, &request_id)) { /* We already have a request pending for this (connection, setting) */ error = g_error_new(NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_FAILED, "Request for %s secrets already pending", request_id); callback(agent, connection, NULL, error, callback_data); return; } if (!(flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION)) { /* We don't do stored passwords */ error = g_error_new(NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_NO_SECRETS, "Stored passwords not supported"); callback(agent, connection, NULL, error, callback_data); return; } nm_assert(g_str_has_suffix(request_id, setting_name)); request_id_setting_name = &request_id[strlen(request_id) - strlen(setting_name)]; nm_assert(nm_streq(request_id_setting_name, setting_name)); request = g_slice_new(RequestData); *request = (RequestData){ .self = self, .connection = g_object_ref(connection), .setting_name = request_id_setting_name, .hints = g_strdupv((char **) hints), .callback = callback, .callback_data = callback_data, .request_id = g_steal_pointer(&request_id), .flags = flags, .cancellable = g_cancellable_new(), }; g_hash_table_add(priv->requests, request); if (priv->enabled) request_secrets_from_ui(request); } /** * nm_secret_agent_simple_response: * @self: the #NMSecretAgentSimple * @request_id: the request ID being responded to * @secrets: (nullable): the array of secrets, or %NULL * * Response to a #NMSecretAgentSimple::get-secrets signal. * * If the user provided secrets, the caller should set the * corresponding value fields in the * #NMSecretAgentSimpleSecrets (freeing any initial values they had), and * pass the array to nm_secret_agent_simple_response(). If the user * cancelled the request, @secrets should be NULL. */ void nm_secret_agent_simple_response(NMSecretAgentSimple *self, const char *request_id, GPtrArray *secrets) { NMSecretAgentSimplePrivate *priv; RequestData *request; gs_unref_variant GVariant *secrets_dict = NULL; gs_free_error GError *error = NULL; int i; g_return_if_fail(NM_IS_SECRET_AGENT_SIMPLE(self)); priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); request = g_hash_table_lookup(priv->requests, &request_id); g_return_if_fail(request != NULL); if (secrets) { GVariantBuilder conn_builder, *setting_builder; GVariantBuilder vpn_secrets_builder; GVariantBuilder wg_secrets_builder; GVariantBuilder wg_peer_builder; GHashTable *settings; GHashTableIter iter; const char *name; gboolean has_vpn = FALSE; gboolean has_wg = FALSE; settings = g_hash_table_new_full(nm_str_hash, g_str_equal, NULL, (GDestroyNotify) g_variant_builder_unref); for (i = 0; i < secrets->len; i++) { SecretReal *secret = secrets->pdata[i]; setting_builder = g_hash_table_lookup(settings, nm_setting_get_name(secret->setting)); if (!setting_builder) { setting_builder = g_variant_builder_new(NM_VARIANT_TYPE_SETTING); g_hash_table_insert(settings, (char *) nm_setting_get_name(secret->setting), setting_builder); } switch (secret->base.secret_type) { case NM_SECRET_AGENT_SECRET_TYPE_PROPERTY: case NM_SECRET_AGENT_SECRET_TYPE_SECRET: g_variant_builder_add(setting_builder, "{sv}", secret->property, g_variant_new_string(secret->base.value)); break; case NM_SECRET_AGENT_SECRET_TYPE_VPN_SECRET: if (!has_vpn) { g_variant_builder_init(&vpn_secrets_builder, G_VARIANT_TYPE("a{ss}")); has_vpn = TRUE; } g_variant_builder_add(&vpn_secrets_builder, "{ss}", secret->property, secret->base.value); break; case NM_SECRET_AGENT_SECRET_TYPE_WIREGUARD_PEER_PSK: if (!has_wg) { g_variant_builder_init(&wg_secrets_builder, G_VARIANT_TYPE("aa{sv}")); has_wg = TRUE; } g_variant_builder_init(&wg_peer_builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&wg_peer_builder, "{sv}", NM_WIREGUARD_PEER_ATTR_PUBLIC_KEY, g_variant_new_string(secret->property)); g_variant_builder_add(&wg_peer_builder, "{sv}", NM_WIREGUARD_PEER_ATTR_PRESHARED_KEY, g_variant_new_string(secret->base.value)); g_variant_builder_add(&wg_secrets_builder, "a{sv}", &wg_peer_builder); break; } } if (has_vpn) { g_variant_builder_add(setting_builder, "{sv}", "secrets", g_variant_builder_end(&vpn_secrets_builder)); } if (has_wg) { g_variant_builder_add(setting_builder, "{sv}", NM_SETTING_WIREGUARD_PEERS, g_variant_builder_end(&wg_secrets_builder)); } g_variant_builder_init(&conn_builder, NM_VARIANT_TYPE_CONNECTION); g_hash_table_iter_init(&iter, settings); while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &setting_builder)) g_variant_builder_add(&conn_builder, "{sa{sv}}", name, setting_builder); secrets_dict = g_variant_ref_sink(g_variant_builder_end(&conn_builder)); g_hash_table_destroy(settings); } else { error = g_error_new(NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_USER_CANCELED, "User cancelled"); } _request_data_complete(request, secrets_dict, error, NULL); } static void cancel_get_secrets(NMSecretAgentOld *agent, const char *connection_path, const char *setting_name) { NMSecretAgentSimple *self = NM_SECRET_AGENT_SIMPLE(agent); NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); gs_free_error GError *error = NULL; gs_free char *request_id = NULL; RequestData *request; request_id = g_strdup_printf("%s/%s", connection_path, setting_name); request = g_hash_table_lookup(priv->requests, &request_id); if (!request) { /* this is really a bug of the caller (or us?). We cannot invoke a callback, * hence the caller cannot cleanup the request. */ g_return_if_reached(); } g_set_error(&error, NM_SECRET_AGENT_ERROR, NM_SECRET_AGENT_ERROR_AGENT_CANCELED, "The secret agent is going away"); _request_data_complete(request, NULL, error, NULL); } static void save_secrets(NMSecretAgentOld *agent, NMConnection *connection, const char *connection_path, NMSecretAgentOldSaveSecretsFunc callback, gpointer callback_data) { /* We don't support secret storage */ callback(agent, connection, NULL, callback_data); } static void delete_secrets(NMSecretAgentOld *agent, NMConnection *connection, const char *connection_path, NMSecretAgentOldDeleteSecretsFunc callback, gpointer callback_data) { /* We don't support secret storage, so there's nothing to delete. */ callback(agent, connection, NULL, callback_data); } /** * nm_secret_agent_simple_enable: * @self: the #NMSecretAgentSimple * @path: (nullable): the path of the connection (if any) to handle secrets * for. If %NULL, secrets for any connection will be handled. * * Enables servicing the requests including the already queued ones. If @path * is given, the agent will only handle requests for connections that match * @path. */ void nm_secret_agent_simple_enable(NMSecretAgentSimple *self, const char *path) { NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(self); gs_free RequestData **requests = NULL; gsize i; gs_free char *path_full = NULL; /* The path is only used to match a request_id with the current * connection. Since the request_id is "${CONNECTION_PATH}/${SETTING}", * add a trailing '/' to the path to match the full connection path. */ path_full = path ? g_strdup_printf("%s/", path) : NULL; if (!nm_streq0(path_full, priv->path)) { g_free(priv->path); priv->path = g_steal_pointer(&path_full); } if (priv->enabled) return; priv->enabled = TRUE; /* Service pending secret requests. */ requests = (RequestData **) g_hash_table_get_keys_as_array(priv->requests, NULL); for (i = 0; requests[i]; i++) request_secrets_from_ui(requests[i]); } /*****************************************************************************/ static void nm_secret_agent_simple_init(NMSecretAgentSimple *agent) { NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(agent); G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(RequestData, request_id) == 0); priv->requests = g_hash_table_new_full(nm_pstr_hash, nm_pstr_equal, NULL, _request_data_free); } /** * nm_secret_agent_simple_new: * @name: the identifier of secret agent * * Creates a new #NMSecretAgentSimple. It does not serve any requests until * nm_secret_agent_simple_enable() is called. * * Returns: a new #NMSecretAgentSimple if the agent creation is successful * or %NULL in case of a failure. */ NMSecretAgentSimple * nm_secret_agent_simple_new(const char *name) { return g_initable_new(NM_TYPE_SECRET_AGENT_SIMPLE, NULL, NULL, NM_SECRET_AGENT_OLD_IDENTIFIER, name, NM_SECRET_AGENT_OLD_CAPABILITIES, NM_SECRET_AGENT_CAPABILITY_VPN_HINTS, NULL); } static void dispose(GObject *object) { NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(object); gs_free_error GError *error = NULL; GHashTableIter iter; RequestData *request; g_hash_table_iter_init(&iter, priv->requests); while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &request)) { if (!error) nm_utils_error_set_cancelled(&error, TRUE, "NMSecretAgentSimple"); _request_data_complete(request, NULL, error, &iter); } G_OBJECT_CLASS(nm_secret_agent_simple_parent_class)->dispose(object); } static void finalize(GObject *object) { NMSecretAgentSimplePrivate *priv = NM_SECRET_AGENT_SIMPLE_GET_PRIVATE(object); g_hash_table_destroy(priv->requests); g_free(priv->path); G_OBJECT_CLASS(nm_secret_agent_simple_parent_class)->finalize(object); } void nm_secret_agent_simple_class_init(NMSecretAgentSimpleClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); NMSecretAgentOldClass *agent_class = NM_SECRET_AGENT_OLD_CLASS(klass); object_class->dispose = dispose; object_class->finalize = finalize; agent_class->get_secrets = get_secrets; agent_class->cancel_get_secrets = cancel_get_secrets; agent_class->save_secrets = save_secrets; agent_class->delete_secrets = delete_secrets; /** * NMSecretAgentSimple::request-secrets: * @agent: the #NMSecretAgentSimple * @request_id: request ID, to eventually pass to * nm_secret_agent_simple_response(). * @title: a title for the password dialog * @prompt: a prompt message for the password dialog * @secrets: (element-type #NMSecretAgentSimpleSecret): array of secrets * being requested. * * Emitted when the agent requires secrets from the user. * * The application should ask user for the secrets. For example, * nmtui should create a password dialog (#NmtPasswordDialog) * with the given title and prompt, and an entry for each * element of @secrets. If any of the secrets already have a * value filled in, the corresponding entry * should be initialized to that value. * * When the dialog is complete, the app must call * nm_secret_agent_simple_response() with the results. */ signals[REQUEST_SECRETS] = g_signal_new(NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS, G_TYPE_FROM_CLASS(klass), 0, 0, NULL, NULL, NULL, G_TYPE_NONE, 4, G_TYPE_STRING, /* request_id */ G_TYPE_STRING, /* title */ G_TYPE_STRING, /* prompt */ G_TYPE_PTR_ARRAY); }