Tactility/Modules/wifi-module/source/wifi_settings.cpp
Ken Van Hoeylandt 6e437d3288
Location services, kernel Device improvements, de-incubate devices, and more (#691)
**New Features**
  * Added location services with GPS and network-based positioning, approximate or precise accuracy, and live status updates.
  * Added a Location Settings app to view status, control location access and accuracy, and manage GPS devices.
  * Added location choices during device setup and a location toggle in the system menu.
  * Added support for thread stacks in external memory when available, with fallback to existing memory options.

**Improvements**
  * Wi-Fi and Bluetooth enabled states are saved when changed and applied at startup; separate “Enable on boot” switches are no longer shown in their management screens.
  * GPS setup can stop promptly when interrupted.
  * Improved device removal handling during startup and while references are outstanding.
  * Updated several device listings to no longer be marked as incubating.
2026-10-13 23:13:30 +02:00

337 lines
11 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include <wifi/wifi_settings.h>
#include <wifi/private/wifi_service.h>
#include <crypt/crypt.h>
#include <service/paths.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <tactility/properties_file.h>
#include <dirent.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
constexpr auto* TAG = "wifi_settings";
constexpr auto* AP_FILE_SUFFIX = ".ap.properties";
constexpr auto* AP_PROPERTIES_KEY_SSID = "ssid";
constexpr auto* AP_PROPERTIES_KEY_PASSWORD = "password";
constexpr auto* AP_PROPERTIES_KEY_AUTO_CONNECT = "autoConnect";
constexpr auto* AP_PROPERTIES_KEY_CHANNEL = "channel";
constexpr auto* SETTINGS_FILE_NAME = "settings.properties";
constexpr auto* SETTINGS_KEY_ENABLE_ON_BOOT = "enableOnBoot";
constexpr size_t CRYPT_BLOCK_SIZE = 16;
// The password is padded to a multiple of the block size before encryption
constexpr size_t ENCRYPTED_PASSWORD_LIMIT = ((WIFI_SETTINGS_PASSWORD_LIMIT + CRYPT_BLOCK_SIZE - 1) / CRYPT_BLOCK_SIZE) * CRYPT_BLOCK_SIZE;
constexpr size_t ENCRYPTED_PASSWORD_HEX_LIMIT = ENCRYPTED_PASSWORD_LIMIT * 2;
static bool enable_on_boot_cached = false;
static bool enable_on_boot = false;
static bool is_file(const char* path) {
struct stat path_stat {};
return stat(path, &path_stat) == 0 && S_ISREG(path_stat.st_mode);
}
error_t wifi_ensure_directory_exists(const char* path) {
char buffer[FILE_MAX_PATH_STRING_LENGTH];
if (strlen(path) >= sizeof(buffer)) {
return ERROR_BUFFER_OVERFLOW;
}
strcpy(buffer, path);
for (char* p = buffer + 1; *p != '\0'; p++) {
if (*p == '/') {
*p = '\0';
if (mkdir(buffer, 0755) != 0 && errno != EEXIST) {
return ERROR_RESOURCE;
}
*p = '/';
}
}
if (mkdir(buffer, 0755) != 0 && errno != EEXIST) {
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
// TODO: The SSID could contain invalid filename characters (e.g. "/", "\" and more)
static error_t get_ap_file_path(const char* ssid, char* out_path, size_t out_path_size) {
char directory[FILE_MAX_PATH_STRING_LENGTH];
error_t error = service_paths_get_user_data_directory(WIFI_SERVICE_ID, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = snprintf(out_path, out_path_size, "%s/%s%s", directory, ssid, AP_FILE_SUFFIX);
if (written < 0 || static_cast<size_t>(written) >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
// The IV is derived from the SSID rather than the password/ciphertext, because the SSID is the one
// value that's known and identical at both encrypt time (save) and decrypt time (load).
static bool encrypt_password(const char* ssid, const char* password, char out_hex[ENCRYPTED_PASSWORD_HEX_LIMIT + 1]) {
const size_t length = strlen(password);
const size_t encrypted_length = ((length + CRYPT_BLOCK_SIZE - 1) / CRYPT_BLOCK_SIZE) * CRYPT_BLOCK_SIZE;
uint8_t padded_password[ENCRYPTED_PASSWORD_LIMIT] = {};
memcpy(padded_password, password, length);
uint8_t encrypted[ENCRYPTED_PASSWORD_LIMIT];
uint8_t iv[16];
crypt_get_iv(ssid, strlen(ssid), iv);
if (crypt_encrypt(iv, padded_password, encrypted, encrypted_length) != 0) {
LOG_E(TAG, "Failed to encrypt");
return false;
}
for (size_t i = 0; i < encrypted_length; i++) {
snprintf(&out_hex[i * 2], 3, "%02x", encrypted[i]);
}
out_hex[encrypted_length * 2] = '\0';
return true;
}
static bool decrypt_password(const char* ssid, const char* hex, char out_password[WIFI_SETTINGS_PASSWORD_LIMIT + 1]) {
const size_t hex_length = strlen(hex);
if (hex_length % 2 != 0 || hex_length > ENCRYPTED_PASSWORD_HEX_LIMIT) {
LOG_E(TAG, "Invalid encrypted password length");
return false;
}
const size_t encrypted_length = hex_length / 2;
uint8_t encrypted[ENCRYPTED_PASSWORD_LIMIT];
char byte_hex[3] = {};
for (size_t i = 0; i < encrypted_length; i++) {
byte_hex[0] = hex[i * 2];
byte_hex[1] = hex[i * 2 + 1];
encrypted[i] = static_cast<uint8_t>(strtoul(byte_hex, nullptr, 16));
}
uint8_t iv[16];
crypt_get_iv(ssid, strlen(ssid), iv);
// Space for the null terminator
uint8_t decrypted[ENCRYPTED_PASSWORD_LIMIT + 1] = {};
int result = crypt_decrypt(iv, encrypted, decrypted, encrypted_length);
if (result != 0) {
LOG_E(TAG, "Failed to decrypt credentials for \"%s\": %d", ssid, result);
return false;
}
strncpy(out_password, reinterpret_cast<char*>(decrypted), WIFI_SETTINGS_PASSWORD_LIMIT);
out_password[WIFI_SETTINGS_PASSWORD_LIMIT] = '\0';
return true;
}
extern "C" {
bool wifi_settings_contains(const char* ssid) {
char path[FILE_MAX_PATH_STRING_LENGTH];
return get_ap_file_path(ssid, path, sizeof(path)) == ERROR_NONE && is_file(path);
}
error_t wifi_settings_load(const char* ssid, WifiApSettings* settings) {
char path[FILE_MAX_PATH_STRING_LENGTH];
error_t error = get_ap_file_path(ssid, path, sizeof(path));
if (error != ERROR_NONE) {
return error;
}
if (!is_file(path)) {
return ERROR_NOT_FOUND;
}
PropertiesFile* file = properties_file_open(path);
if (file == nullptr) {
LOG_E(TAG, "Failed to load properties from %s", path);
return ERROR_RESOURCE;
}
WifiApSettings loaded = {
.ssid = {},
.password = {},
.auto_connect = true,
.channel = 0
};
char value[ENCRYPTED_PASSWORD_HEX_LIMIT + 1];
error = ERROR_NONE;
if (properties_file_get(file, AP_PROPERTIES_KEY_SSID, loaded.ssid, sizeof(loaded.ssid)) != ERROR_NONE) {
LOG_E(TAG, "File does not contain a valid SSID: %s", path);
error = ERROR_RESOURCE;
} else if (properties_file_get(file, AP_PROPERTIES_KEY_PASSWORD, value, sizeof(value)) == ERROR_NONE &&
value[0] != '\0' &&
!decrypt_password(ssid, value, loaded.password)) {
LOG_E(TAG, "Failed to decrypt password from %s", path);
error = ERROR_RESOURCE;
} else {
if (properties_file_get(file, AP_PROPERTIES_KEY_AUTO_CONNECT, value, sizeof(value)) == ERROR_NONE) {
loaded.auto_connect = strcmp(value, "true") == 0;
}
if (properties_file_get(file, AP_PROPERTIES_KEY_CHANNEL, value, sizeof(value)) == ERROR_NONE) {
loaded.channel = static_cast<int32_t>(strtol(value, nullptr, 10));
}
}
properties_file_close(file);
if (error == ERROR_NONE) {
*settings = loaded;
}
return error;
}
error_t wifi_settings_save(const WifiApSettings* settings) {
if (settings->ssid[0] == '\0') {
return ERROR_INVALID_ARGUMENT;
}
char directory[FILE_MAX_PATH_STRING_LENGTH];
char path[FILE_MAX_PATH_STRING_LENGTH];
if (service_paths_get_user_data_directory(WIFI_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE ||
get_ap_file_path(settings->ssid, path, sizeof(path)) != ERROR_NONE) {
return ERROR_RESOURCE;
}
if (wifi_ensure_directory_exists(directory) != ERROR_NONE) {
LOG_E(TAG, "Failed to create %s", directory);
return ERROR_RESOURCE;
}
char password_encrypted[ENCRYPTED_PASSWORD_HEX_LIMIT + 1] = {};
if (settings->password[0] != '\0' && !encrypt_password(settings->ssid, settings->password, password_encrypted)) {
return ERROR_RESOURCE;
}
PropertiesFile* file = properties_file_open(path);
if (file == nullptr) {
LOG_E(TAG, "Failed to open %s", path);
return ERROR_RESOURCE;
}
char channel[12];
snprintf(channel, sizeof(channel), "%ld", static_cast<long>(settings->channel));
properties_file_set(file, AP_PROPERTIES_KEY_SSID, settings->ssid);
properties_file_set(file, AP_PROPERTIES_KEY_PASSWORD, password_encrypted);
properties_file_set(file, AP_PROPERTIES_KEY_AUTO_CONNECT, settings->auto_connect ? "true" : "false");
properties_file_set(file, AP_PROPERTIES_KEY_CHANNEL, channel);
return properties_file_close(file);
}
error_t wifi_settings_remove(const char* ssid) {
char path[FILE_MAX_PATH_STRING_LENGTH];
error_t error = get_ap_file_path(ssid, path, sizeof(path));
if (error != ERROR_NONE) {
return error;
}
if (!is_file(path)) {
return ERROR_NOT_FOUND;
}
return remove(path) == 0 ? ERROR_NONE : ERROR_RESOURCE;
}
void wifi_settings_for_each(void* context, bool (*on_ssid)(const char* ssid, void* context)) {
char directory[FILE_MAX_PATH_STRING_LENGTH];
if (service_paths_get_user_data_directory(WIFI_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE) {
return;
}
DIR* dir = opendir(directory);
if (dir == nullptr) {
return; // No settings saved yet
}
const size_t suffix_length = strlen(AP_FILE_SUFFIX);
dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
size_t name_length = strlen(entry->d_name);
if (name_length <= suffix_length || strcmp(entry->d_name + name_length - suffix_length, AP_FILE_SUFFIX) != 0) {
continue;
}
char path[FILE_MAX_PATH_STRING_LENGTH];
int written = snprintf(path, sizeof(path), "%s/%s", directory, entry->d_name);
if (written < 0 || static_cast<size_t>(written) >= sizeof(path) || !is_file(path)) {
continue;
}
PropertiesFile* file = properties_file_open(path);
if (file == nullptr) {
continue;
}
char ssid[WIFI_SETTINGS_SSID_LIMIT + 1];
bool has_ssid = properties_file_get(file, AP_PROPERTIES_KEY_SSID, ssid, sizeof(ssid)) == ERROR_NONE;
properties_file_close(file);
if (has_ssid && !on_ssid(ssid, context)) {
break;
}
}
closedir(dir);
}
error_t wifi_settings_set_enable(bool enable) {
enable_on_boot = enable;
enable_on_boot_cached = true;
char directory[FILE_MAX_PATH_STRING_LENGTH];
char path[FILE_MAX_PATH_STRING_LENGTH];
if (service_paths_get_user_data_directory(WIFI_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE ||
service_paths_get_user_data_path(WIFI_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE) {
return ERROR_RESOURCE;
}
if (wifi_ensure_directory_exists(directory) != ERROR_NONE) {
LOG_E(TAG, "Failed to create %s", directory);
return ERROR_RESOURCE;
}
PropertiesFile* file = properties_file_open(path);
if (file == nullptr) {
LOG_E(TAG, "Failed to open %s", path);
return ERROR_RESOURCE;
}
properties_file_set(file, SETTINGS_KEY_ENABLE_ON_BOOT, enable ? "true" : "false");
return properties_file_close(file);
}
bool wifi_settings_get_enable(void) {
if (enable_on_boot_cached) {
return enable_on_boot;
}
char path[FILE_MAX_PATH_STRING_LENGTH];
if (service_paths_get_user_data_path(WIFI_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE || !is_file(path)) {
return enable_on_boot;
}
PropertiesFile* file = properties_file_open(path);
if (file == nullptr) {
LOG_I(TAG, "Failed to load settings, using defaults");
return enable_on_boot;
}
char value[8];
if (properties_file_get(file, SETTINGS_KEY_ENABLE_ON_BOOT, value, sizeof(value)) == ERROR_NONE) {
enable_on_boot = strcmp(value, "true") == 0;
enable_on_boot_cached = true;
} else {
LOG_I(TAG, "Failed to load settings, using defaults");
}
properties_file_close(file);
return enable_on_boot;
}
}