diff options
Diffstat (limited to 'src/libnmc-base')
| -rw-r--r-- | src/libnmc-base/nm-client-utils.c | 152 | ||||
| -rw-r--r-- | src/libnmc-base/nm-client-utils.h | 12 | ||||
| -rw-r--r-- | src/libnmc-base/tests/meson.build | 26 | ||||
| -rw-r--r-- | src/libnmc-base/tests/test-client-utils.c | 126 |
4 files changed, 304 insertions, 12 deletions
diff --git a/src/libnmc-base/nm-client-utils.c b/src/libnmc-base/nm-client-utils.c index 10c2c295..9c825994 100644 --- a/src/libnmc-base/nm-client-utils.c +++ b/src/libnmc-base/nm-client-utils.c @@ -697,10 +697,29 @@ NM_PRAGMA_WARNING_DISABLE("-Wdeclaration-after-statement") NM_PRAGMA_WARNING_REENABLE #pragma GCC visibility pop +/* Encode @str into @qrcode (sized qrcodegen_BUFFER_LEN_FOR_VERSION(VERSION_MAX)). + * Returns the QR side length in modules, or -1 if @str is too long to encode. */ +static int +_qr_encode(const char *str, uint8_t *qrcode) +{ + uint8_t tempBuffer[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)]; + + if (!qrcodegen_encodeText(str, + tempBuffer, + qrcode, + qrcodegen_Ecc_LOW, + qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MAX, + qrcodegen_Mask_AUTO, + FALSE)) + return -1; + + return qrcodegen_getSize(qrcode); +} + void nmc_print_qrcode(const char *str) { - uint8_t tempBuffer[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)]; uint8_t qrcode[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)]; gboolean term_linux; int size; @@ -711,18 +730,9 @@ nmc_print_qrcode(const char *str) if (!term_linux && !can_show_graphics()) return; - if (!qrcodegen_encodeText(str, - tempBuffer, - qrcode, - qrcodegen_Ecc_LOW, - qrcodegen_VERSION_MIN, - qrcodegen_VERSION_MAX, - qrcodegen_Mask_AUTO, - FALSE)) { + size = _qr_encode(str, qrcode); + if (size < 0) return; - } - - size = qrcodegen_getSize(qrcode); g_print("\n"); @@ -756,6 +766,124 @@ nmc_print_qrcode(const char *str) } } +static void +_qr_append_mecard(GString *string, const char *tag, const char *text) +{ + const char *p; + bool is_hex = TRUE; + int start; + + if (!text) + return; + + g_string_append(string, tag); + start = string->len; + + for (p = text; *p; p++) { + if (!g_ascii_isxdigit(*p)) + is_hex = FALSE; + if (strchr("\\\":;,", *p)) + g_string_append_c(string, '\\'); + g_string_append_c(string, *p); + } + + if (is_hex && text[0]) { + g_string_insert_c(string, start, '\"'); + g_string_append_c(string, '\"'); + } + g_string_append_c(string, ';'); +} + +/** + * nmc_wifi_qr_uri_new: + * @ssid: the network SSID (UTF-8) + * @key_mgmt: the wireless security key management, or %NULL for an open network + * @psk: the pre-shared key, or %NULL + * @hidden: whether the network is hidden + * + * Builds a "WIFI:" provisioning URI (the format encoded in Wi-Fi QR codes). + * + * Returns: (transfer full): a newly-allocated URI string. + */ +char * +nmc_wifi_qr_uri_new(const char *ssid, const char *key_mgmt, const char *psk, gboolean hidden) +{ + GString *string; + const char *type = NULL; + + string = g_string_sized_new(64); + g_string_append(string, "WIFI:"); + + if (key_mgmt == NULL) + type = "nopass"; + else if (NM_IN_STRSET(key_mgmt, "none", "ieee8021x")) + type = "WEP"; + else if (NM_IN_STRSET(key_mgmt, "wpa-none", "wpa-psk", "sae")) + type = "WPA"; + else if (nm_streq(key_mgmt, "owe")) + type = "nopass"; + + _qr_append_mecard(string, "T:", type); + _qr_append_mecard(string, "S:", ssid); + _qr_append_mecard(string, "P:", psk); + + if (hidden) + g_string_append(string, "H:true;"); + + g_string_append_c(string, ';'); + + return g_string_free(string, FALSE); +} + +/** + * nmc_wifi_qr_render_string: + * @str: the text to encode + * + * Encodes @str as a QR code and renders it into a newline-joined string of + * Unicode half-block glyphs. Dark modules are drawn as foreground glyphs and + * light modules as spaces, so the result is scannable on a black-on-white + * surface (unlike nmc_print_qrcode(), which prints ANSI-colored output to a + * terminal). Two module rows are packed per character row. + * + * Returns: (transfer full): the rendered string, or %NULL if @str is too long + * to encode. + */ +char * +nmc_wifi_qr_render_string(const char *str) +{ + uint8_t qrcode[qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)]; + GString *gstr; + int size; + int x; + int y; + + size = _qr_encode(str, qrcode); + if (size < 0) + return NULL; + + gstr = g_string_sized_new(2048); + + for (y = -2; y < size + 2; y += 2) { + for (x = -2; x < size + 2; x++) { + bool top = qrcodegen_getModule(qrcode, x, y); + bool bottom = qrcodegen_getModule(qrcode, x, y + 1); + + if (top && bottom) + g_string_append(gstr, "█"); + else if (top) + g_string_append(gstr, "▀"); + else if (bottom) + g_string_append(gstr, "▄"); + else + g_string_append_c(gstr, ' '); + } + if (y + 2 < size + 2) + g_string_append_c(gstr, '\n'); + } + + return g_string_free(gstr, FALSE); +} + /** * nmc_utils_read_passwd_file: * @passwd_file: file with passwords to parse diff --git a/src/libnmc-base/nm-client-utils.h b/src/libnmc-base/nm-client-utils.h index 7d6ec707..c90219a5 100644 --- a/src/libnmc-base/nm-client-utils.h +++ b/src/libnmc-base/nm-client-utils.h @@ -62,6 +62,18 @@ const char *nmc_password_subst_char(void); void nmc_print_qrcode(const char *str); +char *nmc_wifi_qr_uri_new(const char *ssid, const char *key_mgmt, const char *psk, gboolean hidden); + +char *nmc_wifi_qr_render_string(const char *str); + +/* Whether @key_mgmt denotes a passphrase-protected network (a "WPA"-type QR + * code), i.e. one whose QR code is useless without the password. */ +static inline gboolean +nmc_wifi_key_mgmt_uses_psk(const char *key_mgmt) +{ + return NM_IN_STRSET(key_mgmt, "wpa-psk", "sae", "wpa-none"); +} + GHashTable *nmc_utils_parse_passwd_file(char *contents, gssize *out_error_line, GError **error); GHashTable * diff --git a/src/libnmc-base/tests/meson.build b/src/libnmc-base/tests/meson.build new file mode 100644 index 00000000..e3119563 --- /dev/null +++ b/src/libnmc-base/tests/meson.build @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +exe = executable( + 'test-client-utils', + 'test-client-utils.c', + dependencies: [ + libnm_dep, + glib_dep, + ], + link_with: [ + libnmc_base, + libnm_core_aux_extern, + libnm_core_aux_intern, + libnm_base, + libnm_log_null, + libnm_glib_aux, + libnm_std_aux, + libc_siphash, + ], +) + +test( + 'src/libnmc-base/tests/test-client-utils', + test_script, + args: test_args + [exe.full_path()], +) diff --git a/src/libnmc-base/tests/test-client-utils.c b/src/libnmc-base/tests/test-client-utils.c new file mode 100644 index 00000000..41ea7262 --- /dev/null +++ b/src/libnmc-base/tests/test-client-utils.c @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* + * Copyright (C) 2026 Red Hat, Inc. + */ + +#include "libnm-client-aux-extern/nm-default-client.h" + +#include "libnmc-base/nm-client-utils.h" + +#include "libnm-glib-aux/nm-test-utils.h" + +/*****************************************************************************/ + +static void +_check_uri(const char *ssid, + const char *key_mgmt, + const char *psk, + gboolean hidden, + const char *expected) +{ + gs_free char *uri = nmc_wifi_qr_uri_new(ssid, key_mgmt, psk, hidden); + + g_assert_cmpstr(uri, ==, expected); +} + +static void +test_wifi_qr_uri_type(void) +{ + _check_uri("MyNet", NULL, NULL, FALSE, "WIFI:T:nopass;S:MyNet;;"); + _check_uri("MyNet", "none", NULL, FALSE, "WIFI:T:WEP;S:MyNet;;"); + _check_uri("MyNet", "ieee8021x", NULL, FALSE, "WIFI:T:WEP;S:MyNet;;"); + _check_uri("MyNet", "wpa-psk", "passXY", FALSE, "WIFI:T:WPA;S:MyNet;P:passXY;;"); + _check_uri("MyNet", "wpa-none", "passXY", FALSE, "WIFI:T:WPA;S:MyNet;P:passXY;;"); + _check_uri("MyNet", "sae", "passXY", FALSE, "WIFI:T:WPA;S:MyNet;P:passXY;;"); + _check_uri("MyNet", "owe", NULL, FALSE, "WIFI:T:nopass;S:MyNet;;"); + + /* Unhandled key_mgmt: no T: tag is emitted. */ + _check_uri("MyNet", "wpa-eap", "passXY", FALSE, "WIFI:S:MyNet;P:passXY;;"); +} + +static void +test_wifi_qr_uri_hidden(void) +{ + _check_uri("MyNet", "wpa-psk", "passXY", TRUE, "WIFI:T:WPA;S:MyNet;P:passXY;H:true;;"); +} + +static void +test_wifi_qr_uri_escaping(void) +{ + /* Each of \ " : ; , is backslash-escaped. */ + _check_uri("a;b:c,d\\e\"f", NULL, NULL, FALSE, "WIFI:T:nopass;S:a\\;b\\:c\\,d\\\\e\\\"f;;"); + + /* An all-hex value is wrapped in double quotes (per the MECARD format). */ + _check_uri("MyNet", "wpa-psk", "deadbeef", FALSE, "WIFI:T:WPA;S:MyNet;P:\"deadbeef\";;"); + + /* A 64-hex-character PSK is also quoted. */ + _check_uri("MyNet", + "wpa-psk", + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + FALSE, + "WIFI:T:WPA;S:MyNet;P:\"0123456789abcdef0123456789abcdef0123456789abcdef012345" + "6789abcdef\";;"); + + /* Mixing hex and non-hex disables quoting. */ + _check_uri("MyNet", "wpa-psk", "dead beef", FALSE, "WIFI:T:WPA;S:MyNet;P:dead beef;;"); + + /* An empty value is not quoted (it is not "all-hex"). */ + _check_uri("", NULL, NULL, FALSE, "WIFI:T:nopass;S:;;"); +} + +/*****************************************************************************/ + +static void +test_wifi_qr_render(void) +{ + gs_free char *qr = nmc_wifi_qr_render_string("WIFI:T:WPA;S:MyNet;P:passXY;;"); + gs_strfreev char **lines = NULL; + + g_assert(qr); + /* Multiple rows packed two modules per character. */ + lines = g_strsplit(qr, "\n", -1); + g_assert_cmpint(g_strv_length(lines), >, 1); +} + +static void +test_wifi_qr_render_too_long(void) +{ + /* Lowercase forces byte mode (capacity 2953 at version 40), so 4000 bytes + * exceed it and encoding fails. */ + gs_free char *str = g_strnfill(4000, 'x'); + gs_free char *qr = nmc_wifi_qr_render_string(str); + + g_assert(!qr); +} + +static void +test_wifi_key_mgmt_uses_psk(void) +{ + g_assert(nmc_wifi_key_mgmt_uses_psk("wpa-psk")); + g_assert(nmc_wifi_key_mgmt_uses_psk("sae")); + g_assert(nmc_wifi_key_mgmt_uses_psk("wpa-none")); + + g_assert(!nmc_wifi_key_mgmt_uses_psk(NULL)); + g_assert(!nmc_wifi_key_mgmt_uses_psk("none")); + g_assert(!nmc_wifi_key_mgmt_uses_psk("owe")); + g_assert(!nmc_wifi_key_mgmt_uses_psk("wpa-eap")); +} + +/*****************************************************************************/ + +NMTST_DEFINE(); + +int +main(int argc, char **argv) +{ + nmtst_init(&argc, &argv, TRUE); + + g_test_add_func("/client-utils/wifi-qr/uri/type", test_wifi_qr_uri_type); + g_test_add_func("/client-utils/wifi-qr/uri/hidden", test_wifi_qr_uri_hidden); + g_test_add_func("/client-utils/wifi-qr/uri/escaping", test_wifi_qr_uri_escaping); + g_test_add_func("/client-utils/wifi-qr/render", test_wifi_qr_render); + g_test_add_func("/client-utils/wifi-qr/render-too-long", test_wifi_qr_render_too_long); + g_test_add_func("/client-utils/wifi/key-mgmt-uses-psk", test_wifi_key_mgmt_uses_psk); + + return g_test_run(); +} |