diff --git a/docs/1.0_TEST_MATRIX.md b/docs/1.0_TEST_MATRIX.md index e3e4d1587..0016edd29 100644 --- a/docs/1.0_TEST_MATRIX.md +++ b/docs/1.0_TEST_MATRIX.md @@ -50,7 +50,7 @@ shoulder button. | Network services | Desired/runtime separation; carrier without IPv4 remains acquiring/not-ready; canonical DHCP restart and bounded post-address listener reconciliation; SSH shell and SFTP independent key-only states; canonical transfer roots and modes; Saves read-only; private roots denied; traversal/symlink/path bounds; same-filesystem atomic publication; authorized-key import; quiesce/health; stale FTP/Telnet/FileBrowser/inherited-Samba/WPS/hotspot flags cannot activate listeners; process-aware listener audit; Samba hidden until a materially different source-build candidate passes feasibility | | Wi-Fi profile store | JFFS2 private root; stdin-only bounded replacement and `bloomctl network profile upsert ID`; requested/document ID binding; explicit `network forget ID`; inactive-ID deletion with active-profile refusal; active forget only after a separately confirmed fallback connection; schema/unknown-field/count/size enforcement; WPA2-PSK, explicit open, and manual hidden WPA2; WEP and hidden open denial; unique IDs/SSIDs; redacted status; explicit local credential-free UI list with one active marker; mode-0600 atomic single-profile supplicant projection; symlink, occupied-slot, live-lock, invalid-profile, and prior-byte preservation | | Wi-Fi scan API | Asynchronous start; redacted status; explicit ID-scoped local results; cancel and terminal discard; one active worker; five-second rate limit; 15-second runtime; 64-KiB raw ceiling; 32 unique printable SSIDs; eight-operation history; open/WPA2-PSK/unsupported classification; duplicate signal reconciliation; backend failure, timeout, oversize, unsafe-path, stale/live-lock, and unsupported-hardware handling | -| Wi-Fi UI | Original Mini hidden; Settings, Shell Quick Settings, and GameSwitcher Quick Settings converge on `--settings-wifi`; direct radio toggle removed; bounded fixed-argv profile-list load outside render; connection state and saved-network detail; selectable Scan/Cancel row and read-only discovered networks; active-only 100-ms SDL wake; deferred GameSwitcher until cancellation discard; single-flight nonblocking fixed-argv scan client; 16-KiB output and five-second deadline; child process-group cleanup; controller-owned start/status/results/discard and cancel-during-start sequencing; 250-ms status opportunities; exact profile/scan/ack schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; terminal history discarded; credential and unknown-field rejection; passwords absent from arguments, logs, screenshots, durable Shell state, and evidence | +| Wi-Fi UI | Original Mini hidden; Settings, Shell Quick Settings, and GameSwitcher Quick Settings converge on `--settings-wifi`; direct radio toggle removed; bounded fixed-argv profile-list load outside render; connection state and saved-network detail; selectable Scan/Cancel row and read-only discovered networks; active-only 100-ms SDL wake; deferred GameSwitcher until cancellation discard; single-flight nonblocking fixed-argv scan client; 16-KiB output and five-second deadline; child process-group cleanup; controller-owned start/status/results/discard and cancel-during-start sequencing; 250-ms status opportunities; exact profile/scan/ack schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; terminal history discarded; host-only open/WPA2 profile form with shared keyboard, masked secret, fixed profile-upsert argv, escaped stdin JSON, strict ID/SSID/PSK validation, and explicit secret clearing; credential and unknown-field rejection; passwords absent from arguments, logs, screenshots, durable Shell state, and evidence | | GameSwitcher | MENU open/toggle; Left/Right select; L1/R1 page; A resume; X actions; START Quick Settings; B Home; no inherited brightness/view/delete behavior | | Apps/Tweaks | Every app disposition recorded; normal configuration requires no Tweaks; supervised launch remains bounded | | Persistent state | Versioned atomic publication; exact restoration across Details/game/GameSwitcher/Quick Settings; malformed/oversized/symlink input defaults without Safe Mode | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index b685d1254..9047a1f91 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -574,6 +574,14 @@ GameSwitcher handoff until cancel, terminal status, and discard clear the exact operation. No scan command is invoked from `draw()` and no result can initiate association in this slice. +The next host-only boundary models a selected open or WPA2-PSK network with one +validated non-secret profile ID and the shared bounded keyboard. Password copy +is always masked; submission invokes only fixed `bloomctl network profile +upsert ID` arguments and writes one escaped schema-valid profile document to +stdin. The password and temporary document are explicitly cleared on every +success and failure path. This model has no rendered consumer and does not +connect, project, restart, or forget a profile. + Profile association reuses the one guarded network-transaction authority rather than adding a second recovery mechanism. `begin` copies the exact current supplicant file into a mode-0700 JFFS2-private transaction root, records only diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ecc09ad33..2f83ed0dd 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -394,6 +394,14 @@ controller: B requests cancellation in the background, and GameSwitcher waits until the exact operation reaches discard. Scan result rows are deliberately read-only until the separate guarded association and credential slices land. +Wi-Fi credential code must keep the SSID and non-secret profile ID separate from +the password buffer. The form validates the exact store vocabulary, masks every +password byte in display copy, JSON-escapes printable input, sends the profile +only on stdin to fixed `network profile upsert ID` arguments, and securely clears +both password and temporary document bytes on every path. Do not wire this +synchronous host-tested boundary into the SDL loop; rendered submission requires +a separate nonblocking controller before it can become reachable. + The NET-04 scan slice is asynchronous and keeps result data only under `/tmp`: ```sh diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 84b8cc7cd..12011825b 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -155,4 +155,12 @@ accepts A. A GameSwitcher request during a scan is held until the controller has confirmed and discarded cancellation, so a child UI cannot strand the Shell's operation or inherit its command pipe. +The host-only Wi-Fi profile form accepts only a schema-valid non-secret profile +ID, printable bounded SSID, open or WPA2-PSK security, and the existing hidden +network rule. WPA2 input is masked and never appears in arguments or process +output. The fixed upsert child reads one JSON document from stdin with stdout and +stderr closed to `/dev/null`; parent memory explicitly clears the password and +escaped document before returning, including validation, pipe, fork, execution, +and timeout failures. No runtime surface can invoke this boundary yet. + Security-sensitive migrations and repair operations must be bounded, logged, reversible where possible, and validated before activation. diff --git a/docs/program/BloomOS_Work_Program_v3.json b/docs/program/BloomOS_Work_Program_v3.json index 4a0995372..599013fdf 100644 --- a/docs/program/BloomOS_Work_Program_v3.json +++ b/docs/program/BloomOS_Work_Program_v3.json @@ -885,12 +885,13 @@ 546, 547, 548, - 549 + 549, + 550 ], "risk_tier": 1, "deployment_tier": "hot", "device_gate_required": true, - "last_audited_main": "9b3a847d99daf8baf44d953670d0ff4a18562a60", + "last_audited_main": "6c277d2300494ba8387accbf4bc28f4459640509", "evidence": [ "issue #544 tracks NET-05 on current main after merged NET-04A-D and UI-05; the audit found that Bloom Shell still treats Wi-Fi as an inline toggle whose A and left/right handlers synchronously call bloom-network enable or disable instead of opening the bounded profile and scan UI", "the first host-safe slice adds an explicit local credential-free projection of at most 16 validated profile IDs, printable SSIDs, security classes, hidden flags, and one active marker; the private service never returns a PSK and the Shell model rejects unknown or credential-bearing fields", @@ -902,7 +903,9 @@ "GitHub PR #547 used risk-scoped CI: format passed in 23 seconds, 58 focused shell tests in 43 seconds, native tests in 1 minute 8 seconds, AddressSanitizer in 1 minute 12 seconds, the affected Bloom Shell ARM build in 1 minute 21 seconds, and scoped static analysis in 2 minutes 8 seconds; it merged by squash as 765f2038b7d63e61d6db8bab1a70ce750a07d088", "the following host-only lifecycle controller sequences start, bounded status polling, results, and terminal discard through the single-flight request client; it preserves terminal results in memory while clearing the service operation, and a cancel received during start is deferred until the exact operation ID arrives before status confirmation and discard", "GitHub PR #548 passed format in 18 seconds, 58 focused shell tests in 50 seconds, the affected Bloom Shell ARM build in 1 minute 11 seconds, native tests in 1 minute 20 seconds, AddressSanitizer in 1 minute 26 seconds, and scoped static analysis in 2 minutes 8 seconds; it merged by squash as 9b3a847d99daf8baf44d953670d0ff4a18562a60", - "GitHub PR #549 renders the bounded scan controller through a 100-ms SDL wake without invoking commands from draw or blocking input, adds a selectable Scan action and read-only bounded results, cancels in the background when leaving the panel, and defers GameSwitcher until the exact scan operation is cancelled, confirmed terminal, and discarded; association, credential entry, forget, and radio power remain absent" + "GitHub PR #549 renders the bounded scan controller through a 100-ms SDL wake without invoking commands from draw or blocking input, adds a selectable Scan action and read-only bounded results, cancels in the background when leaving the panel, and defers GameSwitcher until the exact scan operation is cancelled, confirmed terminal, and discarded; association, credential entry, forget, and radio power remain absent", + "GitHub PR #549 passed format in 40 seconds, 106 affected shell tests in 50 seconds, AddressSanitizer in 1 minute 10 seconds, the affected Bloom Shell ARM build in 1 minute 22 seconds, native tests in 1 minute 30 seconds, and scoped static analysis in 2 minutes 3 seconds; it merged by squash as 6c277d2300494ba8387accbf4bc28f4459640509", + "GitHub PR #550 adds a host-only credential-form boundary that accepts only bounded schema-valid open or WPA2 inputs, masks password copy, invokes fixed bloomctl network profile upsert arguments with escaped JSON only on stdin, and explicitly clears password and temporary document bytes on every path; it has no rendered consumer and cannot scan, associate, restart Wi-Fi, forget a profile, or mutate the device" ] }, { diff --git a/src/bloomShell/bloom_shell_wifi_ui.c b/src/bloomShell/bloom_shell_wifi_ui.c index e5ad4f5dd..b0779cbf2 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.c +++ b/src/bloomShell/bloom_shell_wifi_ui.c @@ -36,8 +36,9 @@ static int bounded_string(const cJSON *value, size_t maximum, int printable) static int safe_id(const cJSON *value) { - if (!bounded_string(value, 32, 1) || value->valuestring[0] == '.' || - value->valuestring[0] == '-') + if (!bounded_string(value, 32, 1) || + !((value->valuestring[0] >= 'a' && value->valuestring[0] <= 'z') || + (value->valuestring[0] >= '0' && value->valuestring[0] <= '9'))) return 0; for (const char *cursor = value->valuestring; *cursor != '\0'; ++cursor) if (!((*cursor >= 'a' && *cursor <= 'z') || (*cursor >= '0' && *cursor <= '9') || @@ -46,6 +47,55 @@ static int safe_id(const cJSON *value) return 1; } +static int safe_id_string(const char *value) +{ + if (value == NULL || value[0] == '\0' || strlen(value) > 32 || + !((value[0] >= 'a' && value[0] <= 'z') || + (value[0] >= '0' && value[0] <= '9'))) + return 0; + for (const char *cursor = value; *cursor != '\0'; ++cursor) + if (!((*cursor >= 'a' && *cursor <= 'z') || (*cursor >= '0' && *cursor <= '9') || + *cursor == '.' || *cursor == '_' || *cursor == '-')) + return 0; + return 1; +} + +static int printable_string(const char *value, size_t maximum) +{ + if (value == NULL || value[0] == '\0') + return 0; + size_t length = strnlen(value, maximum + 1); + if (length == 0 || length > maximum) + return 0; + for (size_t index = 0; index < length; ++index) + if ((unsigned char)value[index] < 0x20 || (unsigned char)value[index] > 0x7e) + return 0; + return 1; +} + +static int valid_psk(const char *value) +{ + if (!printable_string(value, 64)) + return 0; + size_t length = strlen(value); + if (length >= 8 && length <= 63) + return 1; + if (length != 64) + return 0; + for (const char *cursor = value; *cursor != '\0'; ++cursor) + if (!((*cursor >= '0' && *cursor <= '9') || (*cursor >= 'a' && *cursor <= 'f') || + (*cursor >= 'A' && *cursor <= 'F'))) + return 0; + return 1; +} + +static void secure_clear(void *value, size_t size) +{ + volatile unsigned char *cursor = value; + while (size-- > 0) + *cursor++ = 0; +} + static int operation_id_string(const char *value) { if (value == NULL || value[0] == '\0') @@ -220,6 +270,82 @@ int bloom_shell_wifi_profiles_load(const char *bloomctl_path, BloomShellWifiUi * return 0; } +int bloom_shell_wifi_profile_form_init(BloomShellWifiProfileForm *form, + const char *profile_id, const char *ssid, + BloomShellWifiSecurity security, int hidden) +{ + if (form == NULL) + return -1; + if (!safe_id_string(profile_id) || !printable_string(ssid, 32) || + (security != BLOOM_SHELL_WIFI_SECURITY_OPEN && + security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK) || + (hidden && security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK)) { + secure_clear(form, sizeof(*form)); + return -1; + } + memset(form, 0, sizeof(*form)); + bloom_ui_keyboard_init(&form->keyboard); + snprintf(form->profile_id, sizeof(form->profile_id), "%s", profile_id); + snprintf(form->ssid, sizeof(form->ssid), "%s", ssid); + form->security = security; + form->hidden = hidden != 0; + return 0; +} + +int bloom_shell_wifi_profile_form_move(BloomShellWifiProfileForm *form, + int horizontal, int vertical) +{ + return form == NULL ? 0 : bloom_ui_keyboard_move(&form->keyboard, horizontal, vertical); +} + +void bloom_shell_wifi_profile_form_cycle_mode(BloomShellWifiProfileForm *form) +{ + if (form != NULL) + bloom_ui_keyboard_cycle_mode(&form->keyboard); +} + +int bloom_shell_wifi_profile_form_append(BloomShellWifiProfileForm *form) +{ + if (form == NULL || form->security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK) + return -1; + return bloom_ui_text_append(form->psk, sizeof(form->psk), + bloom_ui_keyboard_character(&form->keyboard)); +} + +int bloom_shell_wifi_profile_form_backspace(BloomShellWifiProfileForm *form) +{ + if (form == NULL || form->security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK) + return -1; + return bloom_ui_text_backspace(form->psk, sizeof(form->psk)); +} + +int bloom_shell_wifi_profile_form_label(const BloomShellWifiProfileForm *form, + char *label, size_t label_size) +{ + if (form == NULL || label == NULL || label_size == 0) + return -1; + if (form->security == BLOOM_SHELL_WIFI_SECURITY_OPEN) { + int length = snprintf(label, label_size, "Open network - no password"); + return length >= 0 && (size_t)length < label_size ? 0 : -1; + } + if (form->security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK) + return -1; + size_t psk_length = strnlen(form->psk, sizeof(form->psk)); + const size_t prefix_length = strlen("Password: "); + if (psk_length >= sizeof(form->psk) || prefix_length + psk_length >= label_size) + return -1; + memcpy(label, "Password: ", prefix_length); + memset(label + prefix_length, '*', psk_length); + label[prefix_length + psk_length] = '\0'; + return 0; +} + +void bloom_shell_wifi_profile_form_clear(BloomShellWifiProfileForm *form) +{ + if (form != NULL) + secure_clear(form, sizeof(*form)); +} + int bloom_shell_wifi_scan_start_parse(const char *json, BloomShellWifiUi *wifi) { if (json == NULL || wifi == NULL) @@ -400,6 +526,160 @@ static void close_inherited_descriptors(void) close(descriptor); } +static int append_bytes(char *output, size_t capacity, size_t *length, + const char *value, size_t value_length) +{ + if (*length >= capacity || value_length > capacity - *length - 1) + return -1; + memcpy(output + *length, value, value_length); + *length += value_length; + output[*length] = '\0'; + return 0; +} + +static int append_json_string(char *output, size_t capacity, size_t *length, + const char *value) +{ + if (append_bytes(output, capacity, length, "\"", 1) != 0) + return -1; + for (const char *cursor = value; *cursor != '\0'; ++cursor) { + if ((*cursor == '\"' || *cursor == '\\') && + append_bytes(output, capacity, length, "\\", 1) != 0) + return -1; + if (append_bytes(output, capacity, length, cursor, 1) != 0) + return -1; + } + return append_bytes(output, capacity, length, "\"", 1); +} + +static int profile_document(const BloomShellWifiProfileForm *form, + char *output, size_t capacity, size_t *length) +{ + if (form == NULL || output == NULL || capacity == 0 || length == NULL || + !safe_id_string(form->profile_id) || !printable_string(form->ssid, 32) || + (form->security != BLOOM_SHELL_WIFI_SECURITY_OPEN && + form->security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK) || + (form->hidden && form->security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK) || + (form->security == BLOOM_SHELL_WIFI_SECURITY_OPEN && form->psk[0] != '\0') || + (form->security == BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK && !valid_psk(form->psk))) + return -1; + *length = 0; + output[0] = '\0'; + if (append_bytes(output, capacity, length, "{\"id\":", 6) != 0 || + append_json_string(output, capacity, length, form->profile_id) != 0 || + append_bytes(output, capacity, length, ",\"ssid\":", 8) != 0 || + append_json_string(output, capacity, length, form->ssid) != 0 || + append_bytes(output, capacity, length, ",\"security\":\"", 13) != 0) + return -1; + const char *security = form->security == BLOOM_SHELL_WIFI_SECURITY_OPEN + ? "open" + : "wpa2-psk"; + const char *hidden = form->hidden ? "\",\"hidden\":true" : "\",\"hidden\":false"; + if (append_bytes(output, capacity, length, security, strlen(security)) != 0 || + append_bytes(output, capacity, length, hidden, strlen(hidden)) != 0) + return -1; + if (form->security == BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK && + (append_bytes(output, capacity, length, ",\"psk\":", 7) != 0 || + append_json_string(output, capacity, length, form->psk) != 0)) + return -1; + return append_bytes(output, capacity, length, "}\n", 2); +} + +static int write_all(int descriptor, const char *value, size_t length) +{ + while (length > 0) { + ssize_t written = write(descriptor, value, length); + if (written < 0 && errno == EINTR) + continue; + if (written <= 0) + return -1; + value += written; + length -= (size_t)written; + } + return 0; +} + +int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, + BloomShellWifiProfileForm *form) +{ + char document[512]; + size_t document_length = 0; + if (form == NULL) + return -1; + int valid = executable_regular(bloomctl_path) && + profile_document(form, document, sizeof(document), &document_length) == 0; + if (!valid) { + secure_clear(form->psk, sizeof(form->psk)); + secure_clear(document, sizeof(document)); + return -1; + } + int input[2]; + if (pipe(input) != 0) { + secure_clear(form->psk, sizeof(form->psk)); + secure_clear(document, sizeof(document)); + return -1; + } + pid_t child = fork(); + if (child < 0) { + close(input[0]); + close(input[1]); + secure_clear(form->psk, sizeof(form->psk)); + secure_clear(document, sizeof(document)); + return -1; + } + if (child == 0) { + close(input[1]); + if (setpgid(0, 0) != 0 || dup2(input[0], STDIN_FILENO) < 0) + _exit(127); + close(input[0]); + int null_descriptor = open("/dev/null", O_RDWR); + if (null_descriptor < 0 || dup2(null_descriptor, STDOUT_FILENO) < 0 || + dup2(null_descriptor, STDERR_FILENO) < 0) + _exit(127); + if (null_descriptor > STDERR_FILENO) + close(null_descriptor); + close_inherited_descriptors(); + execl(bloomctl_path, bloomctl_path, "network", "profile", "upsert", + form->profile_id, (char *)NULL); + _exit(127); + } + close(input[0]); + int setup_failed = setpgid(child, child) != 0 && errno != EACCES && errno != ESRCH; + if (setup_failed) { + close(input[1]); + kill(child, SIGKILL); + while (waitpid(child, NULL, 0) < 0 && errno == EINTR) { + } + secure_clear(form->psk, sizeof(form->psk)); + secure_clear(document, sizeof(document)); + return -1; + } + struct sigaction ignored = {.sa_handler = SIG_IGN}; + struct sigaction previous; + sigemptyset(&ignored.sa_mask); + int signal_changed = sigaction(SIGPIPE, &ignored, &previous) == 0; + int failed = write_all(input[1], document, document_length) != 0; + close(input[1]); + if (signal_changed) + sigaction(SIGPIPE, &previous, NULL); + secure_clear(form->psk, sizeof(form->psk)); + secure_clear(document, sizeof(document)); + int status = 0; + for (int attempt = 0; attempt < BLOOM_SHELL_WIFI_REQUEST_TIMEOUT_MS / 10; ++attempt) { + pid_t result = waitpid(child, &status, WNOHANG); + if (result == child) + return !failed && WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : -1; + if (result < 0 && errno != EINTR) + return -1; + usleep(10000); + } + kill(-child, SIGKILL); + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) { + } + return -1; +} + int bloom_shell_wifi_request_start(BloomShellWifiRequest *request, const char *bloomctl_path, BloomShellWifiRequestKind kind, diff --git a/src/bloomShell/bloom_shell_wifi_ui.h b/src/bloomShell/bloom_shell_wifi_ui.h index 661e34043..d95b09b7e 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.h +++ b/src/bloomShell/bloom_shell_wifi_ui.h @@ -1,6 +1,8 @@ #ifndef BLOOM_SHELL_WIFI_UI_H #define BLOOM_SHELL_WIFI_UI_H +#include "../bloomUi/bloom_ui_core.h" + #include #include #include @@ -40,6 +42,15 @@ typedef struct { int signal_dbm; } BloomShellWifiNetwork; +typedef struct { + BloomUiKeyboardFocus keyboard; + char profile_id[33]; + char ssid[33]; + char psk[65]; + BloomShellWifiSecurity security; + int hidden; +} BloomShellWifiProfileForm; + typedef struct { int ready; size_t profile_count; @@ -96,6 +107,19 @@ typedef struct { void bloom_shell_wifi_ui_init(BloomShellWifiUi *wifi); int bloom_shell_wifi_profiles_parse(const char *json, BloomShellWifiUi *wifi); int bloom_shell_wifi_profiles_load(const char *bloomctl_path, BloomShellWifiUi *wifi); +int bloom_shell_wifi_profile_form_init(BloomShellWifiProfileForm *form, + const char *profile_id, const char *ssid, + BloomShellWifiSecurity security, int hidden); +int bloom_shell_wifi_profile_form_move(BloomShellWifiProfileForm *form, + int horizontal, int vertical); +void bloom_shell_wifi_profile_form_cycle_mode(BloomShellWifiProfileForm *form); +int bloom_shell_wifi_profile_form_append(BloomShellWifiProfileForm *form); +int bloom_shell_wifi_profile_form_backspace(BloomShellWifiProfileForm *form); +int bloom_shell_wifi_profile_form_label(const BloomShellWifiProfileForm *form, + char *label, size_t label_size); +int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, + BloomShellWifiProfileForm *form); +void bloom_shell_wifi_profile_form_clear(BloomShellWifiProfileForm *form); int bloom_shell_wifi_scan_start_parse(const char *json, BloomShellWifiUi *wifi); int bloom_shell_wifi_scan_status_parse(const char *json, BloomShellWifiUi *wifi); int bloom_shell_wifi_scan_results_parse(const char *json, BloomShellWifiUi *wifi); diff --git a/test/test_bloom_shell_wifi_ui.cpp b/test/test_bloom_shell_wifi_ui.cpp index b86cfd33d..659bb6c81 100644 --- a/test/test_bloom_shell_wifi_ui.cpp +++ b/test/test_bloom_shell_wifi_ui.cpp @@ -27,6 +27,9 @@ TEST(BloomShellWifiUi, ParsesOnlyCredentialFreeBoundedProfiles) EXPECT_NE(0, bloom_shell_wifi_profiles_parse( R"({"schema":1,"service":"bloom-wifi-profiles","profiles":[{"id":"home","ssid":"One","security":"open","hidden":false,"active":true},{"id":"home","ssid":"Two","security":"open","hidden":false,"active":true}]})", &wifi)); + EXPECT_NE(0, bloom_shell_wifi_profiles_parse( + R"({"schema":1,"service":"bloom-wifi-profiles","profiles":[{"id":"_hidden","ssid":"Hidden ID","security":"open","hidden":false,"active":false}]})", + &wifi)); } TEST(BloomShellWifiUi, LoadsProfilesThroughOneFixedCredentialFreeCommand) @@ -62,6 +65,103 @@ TEST(BloomShellWifiUi, LoadsProfilesThroughOneFixedCredentialFreeCommand) std::filesystem::remove_all(directory); } +TEST(BloomShellWifiUi, EditsOnlyMaskedBoundedWifiCredentials) +{ + BloomShellWifiProfileForm form{}; + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-home", "Fixture Home", + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, 0)); + for (int index = 0; index < 8; ++index) + ASSERT_EQ(0, bloom_shell_wifi_profile_form_append(&form)); + EXPECT_STREQ("11111111", form.psk); + char label[96]; + ASSERT_EQ(0, bloom_shell_wifi_profile_form_label(&form, label, sizeof(label))); + EXPECT_STREQ("Password: ********", label); + EXPECT_EQ(std::string::npos, std::string(label).find("11111111")); + ASSERT_EQ(0, bloom_shell_wifi_profile_form_backspace(&form)); + EXPECT_STREQ("1111111", form.psk); + ASSERT_EQ(1, bloom_shell_wifi_profile_form_move(&form, 0, 1)); + bloom_shell_wifi_profile_form_cycle_mode(&form); + ASSERT_EQ(0, bloom_shell_wifi_profile_form_append(&form)); + EXPECT_STREQ("1111111Q", form.psk); + + EXPECT_NE(0, bloom_shell_wifi_profile_form_init( + &form, "../escape", "Fixture", + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, 0)); + EXPECT_STREQ("", form.psk); + EXPECT_NE(0, bloom_shell_wifi_profile_form_init( + &form, "_hidden", "Fixture", + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, 0)); + EXPECT_NE(0, bloom_shell_wifi_profile_form_init( + &form, "fixture", "Fixture\nNetwork", + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, 0)); + EXPECT_NE(0, bloom_shell_wifi_profile_form_init( + &form, "fixture", "Fixture", + BLOOM_SHELL_WIFI_SECURITY_OPEN, 1)); + bloom_shell_wifi_profile_form_clear(&form); +} + +TEST(BloomShellWifiUi, SubmitsFixedProfileArgumentsViaStdinAndClearsSecret) +{ + auto directory = std::filesystem::temp_directory_path() / + ("bloom-shell-wifi-profile-form-" + std::to_string(getpid())); + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + auto script = directory / "bloomctl"; + auto arguments = directory / "arguments"; + auto input_path = directory / "input"; + { + std::ofstream output(script); + output << "#!/bin/sh\nprintf '%s' \"$*\" > '" << arguments.string() + << "'\ncat > '" << input_path.string() << "'\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + BloomShellWifiProfileForm form{}; + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-home", "Fixture \"Home\" \\ WiFi", + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, 0)); + snprintf(form.psk, sizeof(form.psk), "%s", "test\"key\\value"); + ASSERT_EQ(0, bloom_shell_wifi_profile_form_submit(script.c_str(), &form)); + EXPECT_STREQ("", form.psk); + std::ifstream argument_input(arguments); + std::string recorded_arguments; + std::getline(argument_input, recorded_arguments); + EXPECT_EQ("network profile upsert fixture-home", recorded_arguments); + EXPECT_EQ(std::string::npos, recorded_arguments.find("test")); + std::ifstream profile_input(input_path); + std::string recorded_profile; + std::getline(profile_input, recorded_profile); + EXPECT_EQ( + R"({"id":"fixture-home","ssid":"Fixture \"Home\" \\ WiFi","security":"wpa2-psk","hidden":false,"psk":"test\"key\\value"})", + recorded_profile); + + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-open", "Fixture Open", + BLOOM_SHELL_WIFI_SECURITY_OPEN, 0)); + ASSERT_EQ(0, bloom_shell_wifi_profile_form_submit(script.c_str(), &form)); + profile_input = std::ifstream(input_path); + std::getline(profile_input, recorded_profile); + EXPECT_EQ( + R"({"id":"fixture-open","ssid":"Fixture Open","security":"open","hidden":false})", + recorded_profile); + EXPECT_EQ(std::string::npos, recorded_profile.find("psk")); + + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-short", "Fixture Short", + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, 0)); + snprintf(form.psk, sizeof(form.psk), "%s", "short"); + EXPECT_NE(0, bloom_shell_wifi_profile_form_submit(script.c_str(), &form)); + EXPECT_STREQ("", form.psk); + + ASSERT_EQ(0, bloom_shell_wifi_profile_form_init( + &form, "fixture-fail", "Fixture Fail", + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, 0)); + snprintf(form.psk, sizeof(form.psk), "%s", "fixture-password"); + EXPECT_NE(0, bloom_shell_wifi_profile_form_submit("bloomctl", &form)); + EXPECT_STREQ("", form.psk); + std::filesystem::remove_all(directory); +} + TEST(BloomShellWifiUi, TracksOneBoundedAsynchronousScan) { BloomShellWifiUi wifi{};