From b1e17f1348101cc42f7872ff7def2735c886a3ec Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Fri, 24 Apr 2026 10:31:57 +0200 Subject: [PATCH 1/3] agent: Add passphrase storage options Add RequestPassphraseWithOptions support for PSK passphrase requests. The method lets an agent return the passphrase together with options that affect how IWD handles it. Currently the only option is Store. When Store is false, use the passphrase for the current connection attempt but keep it out of the network profile. Fall back to RequestPassphrase if the agent does not implement the new method. --- src/agent.c | 108 +++++++++++++++++++++++++++++++++++++++++++------- src/agent.h | 1 + src/network.c | 12 ++++++ 3 files changed, 107 insertions(+), 14 deletions(-) diff --git a/src/agent.c b/src/agent.c index 0f718b87e..03538db16 100644 --- a/src/agent.c +++ b/src/agent.c @@ -44,11 +44,13 @@ enum agent_request_type { struct agent_request { enum agent_request_type type; struct l_dbus_message *message; + char *path; unsigned int id; void *user_data; void *user_callback; struct l_dbus_message *trigger; agent_request_destroy_func_t destroy; + bool passphrase_with_options; }; struct agent { @@ -126,6 +128,7 @@ static void agent_request_free(void *user_data) if (request->destroy) request->destroy(request->user_data); + l_free(request->path); l_free(request); } @@ -134,19 +137,37 @@ static void passphrase_reply(struct l_dbus_message *reply, { const char *error, *text; char *passphrase = NULL; + bool store = true; enum agent_result result = AGENT_RESULT_FAILED; agent_request_passphrase_func_t user_callback = request->user_callback; if (l_dbus_message_get_error(reply, &error, &text)) goto done; - if (!l_dbus_message_get_arguments(reply, "s", &passphrase)) + if (request->passphrase_with_options) { + struct l_dbus_message_iter options; + struct l_dbus_message_iter value; + const char *key; + + if (!l_dbus_message_get_arguments(reply, "sa{sv}", + &passphrase, &options)) + goto done; + + while (l_dbus_message_iter_next_entry(&options, &key, &value)) { + if (!strcmp(key, "Store")) { + if (!l_dbus_message_iter_get_variant(&value, + "b", &store)) + goto done; + } + } + } else if (!l_dbus_message_get_arguments(reply, "s", &passphrase)) goto done; result = AGENT_RESULT_OK; done: - user_callback(result, passphrase, request->trigger, request->user_data); + user_callback(result, passphrase, store, request->trigger, + request->user_data); } static void user_name_passwd_reply(struct l_dbus_message *reply, @@ -224,6 +245,57 @@ static void agent_free(void *data) } static void agent_send_next_request(struct agent *agent); +static void agent_receive_reply(struct l_dbus_message *message, + void *user_data); + +static struct l_dbus_message *agent_new_passphrase_message(struct agent *agent, + const char *path, bool with_options) +{ + struct l_dbus_message *message; + + message = l_dbus_message_new_method_call(dbus_get_bus(), + agent->owner, + agent->path, + IWD_AGENT_INTERFACE, + with_options ? + "RequestPassphraseWithOptions" : + "RequestPassphrase"); + + l_dbus_message_set_arguments(message, "o", path); + + return message; +} + +static bool agent_retry_passphrase_without_options(struct agent *agent, + struct agent_request *request, + struct l_dbus_message *reply) +{ + const char *error, *text; + + if (request->type != AGENT_REQUEST_TYPE_PASSPHRASE || + !request->passphrase_with_options) + return false; + + if (!l_dbus_message_get_error(reply, &error, &text)) + return false; + + if (strcmp(error, "org.freedesktop.DBus.Error.UnknownMethod") && + strcmp(error, "org.freedesktop.DBus.Error.UnknownInterface")) + return false; + + request->passphrase_with_options = false; + request->message = agent_new_passphrase_message(agent, request->path, + false); + + agent->pending_id = l_dbus_send_with_reply(dbus_get_bus(), + request->message, + agent_receive_reply, + agent, NULL); + + request->message = NULL; + + return true; +} static void request_timeout(struct l_timeout *timeout, void *user_data) { @@ -242,11 +314,17 @@ static void agent_receive_reply(struct l_dbus_message *message, void *user_data) { struct agent *agent = user_data; + struct agent_request *pending; l_debug("agent %p request id %u", agent, agent->pending_id); agent->pending_id = 0; + pending = l_queue_peek_head(agent->requests); + if (pending && agent_retry_passphrase_without_options(agent, pending, + message)) + return; + agent_finalize_pending(agent, message); if (!agent->pending_id) @@ -281,7 +359,9 @@ static unsigned int agent_queue_request(struct agent *agent, int timeout, void *callback, struct l_dbus_message *trigger, void *user_data, - agent_request_destroy_func_t destroy) + agent_request_destroy_func_t destroy, + const char *path, + bool passphrase_with_options) { struct agent_request *request; @@ -289,11 +369,13 @@ static unsigned int agent_queue_request(struct agent *agent, request->type = type; request->message = message; + request->path = l_strdup(path); request->id = ++next_request_id; request->user_data = user_data; request->user_callback = callback; request->trigger = l_dbus_message_ref(trigger); request->destroy = destroy; + request->passphrase_with_options = passphrase_with_options; agent->timeout_secs = timeout; @@ -365,17 +447,12 @@ unsigned int agent_request_passphrase(const char *path, l_debug("agent %p owner %s path %s", agent, agent->owner, agent->path); - message = l_dbus_message_new_method_call(dbus_get_bus(), - agent->owner, - agent->path, - IWD_AGENT_INTERFACE, - "RequestPassphrase"); - - l_dbus_message_set_arguments(message, "o", path); + message = agent_new_passphrase_message(agent, path, true); return agent_queue_request(agent, AGENT_REQUEST_TYPE_PASSPHRASE, message, agent_timeout_input_request(), - callback, trigger, user_data, destroy); + callback, trigger, user_data, destroy, + path, true); } unsigned int agent_request_pkey_passphrase(const char *path, @@ -401,7 +478,8 @@ unsigned int agent_request_pkey_passphrase(const char *path, return agent_queue_request(agent, AGENT_REQUEST_TYPE_PASSPHRASE, message, agent_timeout_input_request(), - callback, trigger, user_data, destroy); + callback, trigger, user_data, destroy, + NULL, false); } unsigned int agent_request_user_name_password(const char *path, @@ -427,7 +505,8 @@ unsigned int agent_request_user_name_password(const char *path, return agent_queue_request(agent, AGENT_REQUEST_TYPE_USER_NAME_PASSWD, message, agent_timeout_input_request(), - callback, trigger, user_data, destroy); + callback, trigger, user_data, destroy, + NULL, false); } unsigned int agent_request_user_password(const char *path, const char *user, @@ -452,7 +531,8 @@ unsigned int agent_request_user_password(const char *path, const char *user, return agent_queue_request(agent, AGENT_REQUEST_TYPE_PASSPHRASE, message, agent_timeout_input_request(), - callback, trigger, user_data, destroy); + callback, trigger, user_data, destroy, + NULL, false); } static bool find_request(const void *a, const void *b) diff --git a/src/agent.h b/src/agent.h index eb08c6b65..220339db5 100644 --- a/src/agent.h +++ b/src/agent.h @@ -29,6 +29,7 @@ enum agent_result { typedef void (*agent_request_passphrase_func_t) (enum agent_result result, const char *passphrase, + bool store, struct l_dbus_message *message, void *user_data); typedef void (*agent_request_user_name_passwd_func_t) (enum agent_result result, diff --git a/src/network.c b/src/network.c index a5a2375a2..1d14c04c5 100644 --- a/src/network.c +++ b/src/network.c @@ -84,6 +84,7 @@ struct network { char **nai_realms; uint8_t *rc_ie; bool sync_settings:1; /* should settings be synced on connect? */ + bool agent_passphrase_transient:1; bool ask_passphrase:1; /* Whether we should force-ask agent */ bool is_hs20:1; bool anqp_pending:1; /* Set if there is a pending ANQP request */ @@ -324,6 +325,7 @@ static bool __network_set_passphrase(struct network *network, network_reset_passphrase(network); network->passphrase = l_strdup(passphrase); + network->agent_passphrase_transient = false; network->sae_pt_19 = network_generate_sae_pt(network, 19); network->sae_pt_20 = network_generate_sae_pt(network, 20); @@ -664,6 +666,7 @@ static int network_load_psk(struct network *network, struct scan_bss *bss) network_reset_passphrase(network); network_reset_psk(network); + network->agent_passphrase_transient = false; network->passphrase = l_steal_ptr(passphrase); network->password_identifier = l_steal_ptr(password_id); @@ -727,6 +730,9 @@ static void network_settings_save(struct network *network, /* We only update the [Security] bits here, wipe the group first */ l_settings_remove_group(settings, "Security"); + if (network->agent_passphrase_transient) + return; + if (network->psk) l_settings_set_bytes(settings, "Security", "PreSharedKey", network->psk, 32); @@ -1316,6 +1322,7 @@ struct scan_bss *network_bss_select(struct network *network, static void passphrase_callback(enum agent_result result, const char *passphrase, + bool store, struct l_dbus_message *message, void *user_data) { @@ -1355,6 +1362,8 @@ static void passphrase_callback(enum agent_result result, goto err; } + network->agent_passphrase_transient = !store; + station_connect_network(station, network, bss, message); l_dbus_message_unref(message); return; @@ -1424,11 +1433,14 @@ static bool eap_secret_info_match_local(const void *a, const void *b) } static void eap_password_callback(enum agent_result result, const char *value, + bool store, struct l_dbus_message *message, void *user_data) { struct eap_secret_request *req = user_data; + (void) store; + req->network->agent_request = 0; if (value) { From c642851004bf4d421ce8e98731cc1d1acd974e7a Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Fri, 24 Apr 2026 10:31:58 +0200 Subject: [PATCH 2/3] doc: Document passphrase storage options Document RequestPassphraseWithOptions and the Store option for agents that want to control whether a PSK passphrase is saved in the network profile. --- doc/agent-api.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/agent-api.txt b/doc/agent-api.txt index e9bb95ca7..77e9bc6da 100644 --- a/doc/agent-api.txt +++ b/doc/agent-api.txt @@ -90,6 +90,18 @@ Methods void Release() [noreply] Possible Errors: net.connman.iwd.Agent.Error.Canceled + (string, dict) RequestPassphraseWithOptions(object network) + + This method gets called when trying to connect to + a network and passphrase is required. Agents + implementing this method can return options for the + passphrase. If the Store boolean option is present + and false, IWD will use the passphrase for this + connection attempt but will not save it to the + network profile. + + Possible Errors: net.connman.iwd.Agent.Error.Canceled + string RequestPrivateKeyPassphrase(object network) This method gets called when connecting to From ebe1aa3e4df02c1ddbfa77972209d2d095b0226e Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Fri, 24 Apr 2026 10:31:59 +0200 Subject: [PATCH 3/3] auto-t: Test transient PSK agent secrets Extend the PSK agent test helper to implement RequestPassphraseWithOptions and add coverage for Store=false. The test verifies that IWD can use the agent-provided passphrase without writing PSK secrets back to the profile. --- .../testEncryptedProfiles/connection_test.py | 27 +++++++++++++++++++ autotests/util/iwd.py | 14 +++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/autotests/testEncryptedProfiles/connection_test.py b/autotests/testEncryptedProfiles/connection_test.py index eb7fd7c5b..17c5ff115 100644 --- a/autotests/testEncryptedProfiles/connection_test.py +++ b/autotests/testEncryptedProfiles/connection_test.py @@ -21,6 +21,17 @@ def profile_is_encrypted(self, profile): return True + def profile_has_psk_secret(self, profile): + with open('/tmp/iwd/' + profile) as f: + contents = f.read() + + return any(secret in contents for secret in [ + 'Passphrase', + 'PreSharedKey', + 'SAE-PT-Group19', + 'SAE-PT-Group20', + ]) + def validate(self, wd): devices = wd.list_devices(1) device = devices[0] @@ -80,6 +91,22 @@ def test_agent_profile(self): wd.unregister_psk_agent(psk_agent) + # Tests that an agent can request one-time use of a passphrase. + def test_agent_transient_profile(self): + wd = IWD(True) + + psk_agent = PSKAgent("secret123", store=False) + wd.register_psk_agent(psk_agent) + + with self.assertRaises(FileNotFoundError): + self.profile_has_psk_secret('ssidCCMP.psk') + + self.validate(wd) + + self.assertFalse(self.profile_has_psk_secret('ssidCCMP.psk')) + + wd.unregister_psk_agent(psk_agent) + # Tests that an invalid profile gets re-written after an agent request def test_invalid_profile_rewritten(self): bad_config = '[Security]\nPassphrase=incorrect\n' diff --git a/autotests/util/iwd.py b/autotests/util/iwd.py index 37eb49434..54096b01c 100755 --- a/autotests/util/iwd.py +++ b/autotests/util/iwd.py @@ -1153,7 +1153,7 @@ def __str__(self): class PSKAgent(dbus.service.Object): - def __init__(self, passphrases=[], users=[], namespace=ctx): + def __init__(self, passphrases=[], users=[], namespace=ctx, store=True): global agent_count if type(passphrases) != list: @@ -1162,6 +1162,7 @@ def __init__(self, passphrases=[], users=[], namespace=ctx): if type(users) != list: users = [users] self.users = users + self.store = store self._path = '/test/agent/%s' % agent_count self._bus = dbus.bus.BusConnection(address_or_type=namespace.dbus_address) @@ -1193,6 +1194,17 @@ def RequestPassphrase(self, path): return self.passphrases.pop(0) + @dbus.service.method(IWD_AGENT_INTERFACE, in_signature='o', + out_signature='sa{sv}') + def RequestPassphraseWithOptions(self, path): + print('Requested PSK with options for ' + path) + + if not self.passphrases: + raise CanceledEx("canceled") + + return (self.passphrases.pop(0), + {'Store': dbus.Boolean(self.store, variant_level=1)}) + @dbus.service.method(IWD_AGENT_INTERFACE, in_signature='o', out_signature='s') def RequestPrivateKeyPassphrase(self, path):