/ firmware / src / services / identity.cpp
identity.cpp
  1  #include <identity.h>
  2  
  3  #include "../boot/provisioning.h"
  4  #include <networking/wifi.h>
  5  
  6  #include <Arduino.h>
  7  #include <Preferences.h>
  8  #include <string.h>
  9  
 10  namespace {
 11  
 12  static char hostname_data[config::shell::HOSTNAME_SIZE + 1] = {};
 13  static bool hostname_initialized = false;
 14  
 15  static bool get_provisioning_string(const char *key,
 16                                      IdentityStringQuery *query) {
 17    if (!query || !query->buffer || query->capacity == 0)
 18      return false;
 19    Preferences prefs;
 20    if (!prefs.begin(config::provisioning::NVS_NAMESPACE, true))
 21      return false;
 22    bool ok = prefs.getString(key, query->buffer, query->capacity) > 0;
 23    prefs.end();
 24    query->ok = ok;
 25    return ok;
 26  }
 27  
 28  static bool set_provisioning_string(const char *key, const char *value) {
 29    Preferences prefs;
 30    if (!prefs.begin(config::provisioning::NVS_NAMESPACE, false))
 31      return false;
 32    prefs.putString(key, value ? value : "");
 33    prefs.end();
 34    return true;
 35  }
 36  
 37  static void ensure_hostname(void) {
 38    if (hostname_initialized)
 39      return;
 40    strncpy(hostname_data, config::HOSTNAME, config::shell::HOSTNAME_SIZE);
 41    hostname_data[config::shell::HOSTNAME_SIZE] = '\0';
 42    hostname_initialized = true;
 43  }
 44  
 45  } // namespace
 46  
 47  void services::identity::initialize() { ensure_hostname(); }
 48  
 49  const char *services::identity::access_hostname() {
 50    ensure_hostname();
 51    return hostname_data;
 52  }
 53  
 54  bool services::identity::configure_hostname(const char *hostname) {
 55    ensure_hostname();
 56    if (!hostname)
 57      return false;
 58  
 59    strncpy(hostname_data, hostname, config::shell::HOSTNAME_SIZE);
 60    hostname_data[config::shell::HOSTNAME_SIZE] = '\0';
 61    networking::wifi::configure_hostname(hostname_data);
 62    return true;
 63  }
 64  
 65  bool services::identity::access_username(IdentityStringQuery *query) {
 66    return get_provisioning_string("username", query);
 67  }
 68  
 69  bool services::identity::configure_username(const char *value) {
 70    return set_provisioning_string("username", value);
 71  }
 72  
 73  bool services::identity::access_device_name(IdentityStringQuery *query) {
 74    return get_provisioning_string("device_name", query);
 75  }
 76  
 77  bool services::identity::configureDeviceName(const char *value) {
 78    return set_provisioning_string("device_name", value);
 79  }
 80  
 81  bool services::identity::accessAPIKey(IdentityStringQuery *query) {
 82    return get_provisioning_string("api_key", query);
 83  }
 84  
 85  bool services::identity::configureAPIKey(const char *value) {
 86    return set_provisioning_string("api_key", value);
 87  }
 88  
 89  bool services::identity::accessSnapshot(DeviceIdentitySnapshot *snapshot) {
 90    if (!snapshot)
 91      return false;
 92    memset(snapshot, 0, sizeof(*snapshot));
 93  
 94    strncpy(snapshot->hostname, services::identity::access_hostname(),
 95            sizeof(snapshot->hostname) - 1);
 96    IdentityStringQuery username_query = {
 97        .buffer = snapshot->username,
 98        .capacity = sizeof(snapshot->username),
 99        .ok = false,
100    };
101    services::identity::access_username(&username_query);
102    IdentityStringQuery device_name_query = {
103        .buffer = snapshot->device_name,
104        .capacity = sizeof(snapshot->device_name),
105        .ok = false,
106    };
107    if (!services::identity::access_device_name(&device_name_query)) {
108      strncpy(snapshot->device_name, config::HOSTNAME,
109              sizeof(snapshot->device_name) - 1);
110    }
111    IdentityStringQuery api_key_query = {
112        .buffer = snapshot->api_key,
113        .capacity = sizeof(snapshot->api_key),
114        .ok = false,
115    };
116    services::identity::accessAPIKey(&api_key_query);
117    snapshot->provisioned = boot::provisioning::isProvisioned();
118    return true;
119  }
120  
121  #ifdef PIO_UNIT_TESTING
122  
123  #include <testing/utils.h>
124  
125  static void test_identity_hostname_default(void) {
126    GIVEN("a freshly booted device");
127    THEN("hostname matches config::HOSTNAME");
128    TEST_ASSERT_EQUAL_STRING_MESSAGE(
129        config::HOSTNAME, services::identity::access_hostname(),
130        "device: boot-time hostname should match config::HOSTNAME");
131  }
132  
133  static void test_identity_hostname_truncation(void) {
134    WHEN("a hostname longer than HOSTNAME_SIZE is set");
135  
136    char long_name[config::shell::HOSTNAME_SIZE + 20];
137    memset(long_name, 'A', sizeof(long_name) - 1);
138    long_name[sizeof(long_name) - 1] = '\0';
139  
140    services::identity::configure_hostname(long_name);
141    size_t result_len = strlen(services::identity::access_hostname());
142    TEST_ASSERT_LESS_OR_EQUAL_UINT32_MESSAGE(
143        config::shell::HOSTNAME_SIZE, result_len,
144        "device: hostname exceeds config::shell::HOSTNAME_SIZE after truncation");
145  
146    services::identity::configure_hostname(config::HOSTNAME);
147  }
148  
149  static void test_identity_username_roundtrip(void) {
150    WHEN("a username is stored and read back");
151    TEST_ASSERT_TRUE_MESSAGE(services::identity::configure_username("alice"),
152                             "device: username should be writable");
153  
154    char username[64] = {0};
155    IdentityStringQuery query = {
156        .buffer = username,
157        .capacity = sizeof(username),
158        .ok = false,
159    };
160    TEST_ASSERT_TRUE_MESSAGE(services::identity::access_username(&query),
161                             "device: username should be readable");
162    TEST_ASSERT_EQUAL_STRING_MESSAGE("alice", username,
163                                     "device: username mismatch after roundtrip");
164  }
165  
166  static void test_identity_device_name_roundtrip(void) {
167    WHEN("a device name is stored and read back");
168    TEST_ASSERT_TRUE_MESSAGE(
169        services::identity::configureDeviceName("ceratina-lab"),
170        "device: device_name should be writable");
171  
172    char device_name[64] = {0};
173    IdentityStringQuery query = {
174        .buffer = device_name,
175        .capacity = sizeof(device_name),
176        .ok = false,
177    };
178    TEST_ASSERT_TRUE_MESSAGE(services::identity::access_device_name(&query),
179                             "device: device_name should be readable");
180    TEST_ASSERT_EQUAL_STRING_MESSAGE(
181        "ceratina-lab", device_name,
182        "device: device_name mismatch after roundtrip");
183  }
184  
185  static void test_identity_api_key_roundtrip(void) {
186    WHEN("an API key is stored and read back");
187    TEST_ASSERT_TRUE_MESSAGE(services::identity::configureAPIKey("secret-key"),
188                             "device: api_key should be writable");
189  
190    char api_key[64] = {0};
191    IdentityStringQuery query = {
192        .buffer = api_key,
193        .capacity = sizeof(api_key),
194        .ok = false,
195    };
196    TEST_ASSERT_TRUE_MESSAGE(services::identity::accessAPIKey(&query),
197                             "device: api_key should be readable");
198    TEST_ASSERT_EQUAL_STRING_MESSAGE("secret-key", api_key,
199                                     "device: api_key mismatch after roundtrip");
200  }
201  
202  void services::identity::test() {
203    MODULE("Identity");
204    RUN_TEST(test_identity_hostname_default);
205    RUN_TEST(test_identity_hostname_truncation);
206    RUN_TEST(test_identity_username_roundtrip);
207    RUN_TEST(test_identity_device_name_roundtrip);
208    RUN_TEST(test_identity_api_key_roundtrip);
209  }
210  
211  #endif