CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
BackupManager.cpp
Go to the documentation of this file.
1
11
14
16#include "cdc_core/IModule.h"
17#include "cdc_core/Raii.h"
18#include "cdc_core/Crypto.h"
19#include "cdc_ui/PsramCjson.h"
20#include "cdc_log.h"
23
24#include "cJSON.h"
25#include <mbedtls/pkcs5.h>
26#include <mbedtls/md.h>
27#include <mbedtls/platform_util.h>
28#include <mbedtls/base64.h>
29#include <esp_random.h>
30
31#include <cstdio>
32#include <cstring>
33#include <sys/stat.h>
34
35namespace cdc::os_ui {
36
37namespace {
38
39constexpr const char* TAG = "Backup";
40
42constexpr uint8_t MAGIC[] = {'C', 'D', 'C', 'B', 'A', 'K'};
43constexpr size_t MAGIC_SIZE = sizeof(MAGIC);
44constexpr uint8_t CONTAINER_VERSION = 1;
45constexpr size_t SALT_SIZE = 16;
46constexpr size_t NONCE_SIZE = 12;
47constexpr size_t TAG_SIZE = 16;
48constexpr size_t KEY_SIZE = 32;
49constexpr uint32_t KDF_ITERATIONS = 200000;
50
53constexpr size_t HEADER_SIZE = MAGIC_SIZE + 1 + 4 + SALT_SIZE + NONCE_SIZE;
54
57constexpr size_t MAX_PAYLOAD = 256 * 1024;
58
60constexpr size_t MAX_CONTAINER = MAX_PAYLOAD + HEADER_SIZE + TAG_SIZE;
61
64constexpr size_t MAX_BASE64 = ((MAX_CONTAINER + 2) / 3) * 4 + 1;
65
66const char* backupPath() {
67 static char path[64] = {0};
68 if (path[0] == '\0') {
69 std::snprintf(path, sizeof(path), "%s/backup.cdcbak",
71 }
72 return path;
73}
74
76void putU32le(uint8_t* p, uint32_t v) {
77 p[0] = static_cast<uint8_t>(v & 0xFF);
78 p[1] = static_cast<uint8_t>((v >> 8) & 0xFF);
79 p[2] = static_cast<uint8_t>((v >> 16) & 0xFF);
80 p[3] = static_cast<uint8_t>((v >> 24) & 0xFF);
81}
82
84uint32_t getU32le(const uint8_t* p) {
85 return static_cast<uint32_t>(p[0]) |
86 (static_cast<uint32_t>(p[1]) << 8) |
87 (static_cast<uint32_t>(p[2]) << 16) |
88 (static_cast<uint32_t>(p[3]) << 24);
89}
90
97bool b64Encode(const uint8_t* src, size_t src_len,
98 uint8_t* dst, size_t dst_cap, size_t* olen) {
99 int ret = mbedtls_base64_encode(dst, dst_cap, olen, src, src_len);
100 if (ret != 0) {
101 LOG_E(TAG, "base64 encode failed: %d", ret);
102 return false;
103 }
104 if (*olen < dst_cap) dst[*olen] = '\0';
105 return true;
106}
107
113bool b64Decode(const uint8_t* src, size_t src_len,
114 uint8_t* dst, size_t dst_cap, size_t* olen) {
115 int ret = mbedtls_base64_decode(dst, dst_cap, olen, src, src_len);
116 if (ret != 0) {
117 LOG_E(TAG, "base64 decode failed: %d", ret);
118 return false;
119 }
120 return true;
121}
122
127bool deriveKey(const char* passphrase, const uint8_t* salt, uint32_t iterations,
128 uint8_t key_out[KEY_SIZE]) {
129 int ret = mbedtls_pkcs5_pbkdf2_hmac_ext(
130 MBEDTLS_MD_SHA256,
131 reinterpret_cast<const uint8_t*>(passphrase), std::strlen(passphrase),
132 salt, SALT_SIZE,
133 iterations, KEY_SIZE, key_out);
134 return ret == 0;
135}
136
143bool sealContainer(const char* passphrase,
144 const uint8_t* plaintext, size_t plaintext_len,
145 uint8_t* out, size_t out_cap, size_t* out_len) {
146 const size_t total = HEADER_SIZE + plaintext_len + TAG_SIZE;
147 if (out_cap < total) return false;
148
149 uint8_t* p_magic = out;
150 uint8_t* p_version = p_magic + MAGIC_SIZE;
151 uint8_t* p_iters = p_version + 1;
152 uint8_t* p_salt = p_iters + 4;
153 uint8_t* p_nonce = p_salt + SALT_SIZE;
154 uint8_t* p_ct = out + HEADER_SIZE;
155 uint8_t* p_tag = p_ct + plaintext_len;
156
157 std::memcpy(p_magic, MAGIC, MAGIC_SIZE);
158 *p_version = CONTAINER_VERSION;
159 putU32le(p_iters, KDF_ITERATIONS);
160 esp_fill_random(p_salt, SALT_SIZE);
161 esp_fill_random(p_nonce, NONCE_SIZE);
162
163 uint8_t key[KEY_SIZE];
164 if (!deriveKey(passphrase, p_salt, KDF_ITERATIONS, key)) {
165 mbedtls_platform_zeroize(key, sizeof(key));
166 return false;
167 }
168
169 bool ok = cdc::core::aesGcm256Seal(
170 key, p_nonce, NONCE_SIZE,
171 out, HEADER_SIZE,
172 plaintext, plaintext_len,
173 p_ct, p_tag);
174 mbedtls_platform_zeroize(key, sizeof(key));
175 if (!ok) {
176 LOG_E(TAG, "GCM encrypt failed");
177 return false;
178 }
179 *out_len = total;
180 return true;
181}
182
189bool openContainer(const char* passphrase,
190 const uint8_t* container, size_t container_len,
191 uint8_t* plaintext_out, size_t plaintext_cap,
192 size_t* plaintext_len) {
193 if (container_len < HEADER_SIZE + TAG_SIZE) return false;
194 if (std::memcmp(container, MAGIC, MAGIC_SIZE) != 0) {
195 LOG_E(TAG, "Bad magic");
196 return false;
197 }
198 if (container[MAGIC_SIZE] != CONTAINER_VERSION) {
199 LOG_E(TAG, "Unsupported container version %u", container[MAGIC_SIZE]);
200 return false;
201 }
202
203 const uint8_t* p_iters = container + MAGIC_SIZE + 1;
204 const uint8_t* p_salt = p_iters + 4;
205 const uint8_t* p_nonce = p_salt + SALT_SIZE;
206 const uint8_t* p_ct = container + HEADER_SIZE;
207 const size_t ct_len = container_len - HEADER_SIZE - TAG_SIZE;
208 const uint8_t* p_tag = p_ct + ct_len;
209 if (ct_len > plaintext_cap) return false;
210
211 uint32_t iterations = getU32le(p_iters);
212
213 uint8_t key[KEY_SIZE];
214 if (!deriveKey(passphrase, p_salt, iterations, key)) {
215 mbedtls_platform_zeroize(key, sizeof(key));
216 return false;
217 }
218
219 bool ok = cdc::core::aesGcm256Open(
220 key, p_nonce, NONCE_SIZE,
221 container, HEADER_SIZE,
222 p_ct, ct_len,
223 p_tag, plaintext_out);
224 mbedtls_platform_zeroize(key, sizeof(key));
225 if (!ok) {
226 mbedtls_platform_zeroize(plaintext_out, ct_len);
227 LOG_W(TAG, "GCM decrypt/auth failed (wrong passphrase?)");
228 return false;
229 }
230 *plaintext_len = ct_len;
231 return true;
232}
233
235uint32_t packApiLevel(const char* str) {
236 unsigned major = 0, minor = 0;
237 if (!str || std::sscanf(str, "%u.%u", &major, &minor) < 1) return 0;
238 return (major << 16) | (minor & 0xFFFF);
239}
240
241} // namespace
242
243BackupManager& BackupManager::instance() {
244 static BackupManager mgr;
245 return mgr;
246}
247
248bool BackupManager::exportTo(const char* passphrase) {
249 if (!passphrase || passphrase[0] == '\0') {
250 LOG_E(TAG, "Empty passphrase");
251 return false;
252 }
253
254 bool ok = false;
255 auto plain = cdc::core::psramAlloc<uint8_t>(MAX_PAYLOAD);
256 auto container = cdc::core::psramAlloc<uint8_t>(MAX_CONTAINER);
257 auto b64buf = cdc::core::psramAlloc<uint8_t>(MAX_BASE64);
258 if (!plain || !container || !b64buf) {
259 LOG_E(TAG, "PSRAM alloc failed");
260 return false;
261 }
262
263 {
265 char* json_text = nullptr;
266 cJSON* root = cJSON_CreateObject();
267 if (!root) return false;
268
269 cJSON_AddStringToObject(root, "host_api_level", HOST_API_LEVEL_STR);
270 cJSON_AddStringToObject(root, "fw_version", APP_VERSION);
271 cJSON* modules = cJSON_AddObjectToObject(root, "modules");
272
274 uint8_t count = reg.getModuleCount();
275 uint8_t exported = 0;
276 for (uint8_t i = 0; modules && i < count; i++) {
277 cdc::core::IModule* m = reg.getModuleAt(i);
278 if (!m || !m->getName()) continue;
279
280 // Each module fills a section node; absent modules emit nothing.
281 cJSON* section = cJSON_CreateObject();
282 if (!section) continue;
283 if (m->exportBackup(section)) {
284 cJSON_AddItemToObject(modules, m->getName(), section);
285 exported++;
286 } else {
287 cJSON_Delete(section);
288 }
289 }
290 LOG_I(TAG, "Exporting %u module section(s)", exported);
291
292 // System/NVS settings have no owning module; emit them as a top-level
293 // "system" section alongside "modules".
294 cJSON* system = cJSON_CreateObject();
295 if (system && SystemSettingsBackup::exportSystemSettings(system)) {
296 cJSON_AddItemToObject(root, "system", system);
297 } else if (system) {
298 cJSON_Delete(system);
299 }
300
301 json_text = cJSON_PrintUnformatted(root);
302 cJSON_Delete(root);
303
304 if (!json_text) {
305 LOG_E(TAG, "JSON serialization failed");
306 return false;
307 }
308
309 size_t plain_len = std::strlen(json_text);
310 if (plain_len <= MAX_PAYLOAD) {
311 std::memcpy(plain.get(), json_text, plain_len);
312 size_t container_len = 0;
313 size_t b64_len = 0;
314 if (sealContainer(passphrase, plain.get(), plain_len,
315 container.get(), MAX_CONTAINER,
316 &container_len) &&
317 b64Encode(container.get(), container_len,
318 b64buf.get(), MAX_BASE64, &b64_len)) {
319 auto f = cdc::core::openFile(backupPath(), "wb");
320 if (f && std::fwrite(b64buf.get(), 1, b64_len, f.get()) == b64_len) {
321 ok = true;
322 LOG_I(TAG, "Backup written (%zu bytes base64)", b64_len);
323 } else {
324 LOG_E(TAG, "Write to %s failed", backupPath());
325 }
326 }
327 } else {
328 LOG_E(TAG, "Payload too large: %zu", plain_len);
329 }
330
331 // Wipe every transient copy of the plaintext before releasing the
332 // buffers. Free json_text inside the scope so it pairs with the PSRAM
333 // hook that allocated it.
334 mbedtls_platform_zeroize(json_text, plain_len);
335 cJSON_free(json_text);
336 }
337
338 mbedtls_platform_zeroize(plain.get(), MAX_PAYLOAD);
339 return ok;
340}
341
343 BackupSummary summary;
344 if (!passphrase || passphrase[0] == '\0' || !backupExists()) {
345 return summary;
346 }
347
348 auto b64buf = cdc::core::psramAlloc<uint8_t>(MAX_BASE64);
349 auto container = cdc::core::psramAlloc<uint8_t>(MAX_CONTAINER);
350 auto plain = cdc::core::psramAlloc<uint8_t>(MAX_PAYLOAD + 1);
351 if (!b64buf || !container || !plain) {
352 LOG_E(TAG, "PSRAM alloc failed");
353 return summary;
354 }
355
356 size_t b64_len = 0;
357 {
358 auto f = cdc::core::openFile(backupPath(), "rb");
359 if (!f) return summary;
360 b64_len = std::fread(b64buf.get(), 1, MAX_BASE64 - 1, f.get());
361 }
362
363 size_t container_len = 0;
364 if (!b64Decode(b64buf.get(), b64_len,
365 container.get(), MAX_CONTAINER, &container_len)) {
366 return summary;
367 }
368
369 size_t plain_len = 0;
370 if (!openContainer(passphrase, container.get(), container_len,
371 plain.get(), MAX_PAYLOAD, &plain_len)) {
372 return summary;
373 }
374 plain.get()[plain_len] = '\0';
375
376 {
378 cJSON* root = cJSON_Parse(reinterpret_cast<const char*>(plain.get()));
379 if (root) {
380 const cJSON* level = cJSON_GetObjectItemCaseSensitive(root, "host_api_level");
381 uint32_t file_level = packApiLevel(cJSON_IsString(level) ? level->valuestring : nullptr);
382 if (file_level > HOST_API_LEVEL_PACKED) {
383 LOG_E(TAG, "Backup host_api_level too new (%s > %s)",
384 cJSON_IsString(level) ? level->valuestring : "?", HOST_API_LEVEL_STR);
385 } else {
386 const cJSON* modules = cJSON_GetObjectItemCaseSensitive(root, "modules");
388 for (const cJSON* sec = modules ? modules->child : nullptr;
389 sec != nullptr; sec = sec->next) {
390 if (!sec->string) continue;
391 cdc::core::IModule* m = reg.getModule(sec->string);
392 if (!m) {
393 LOG_W(TAG, "No module '%s' on device, skipping section", sec->string);
394 summary.skipped++;
395 continue;
396 }
398 summary.imported += r.imported;
399 summary.failed += r.failed;
400 summary.modules++;
401 }
402
403 // Route the top-level "system" section (no owning module).
404 const cJSON* system = cJSON_GetObjectItemCaseSensitive(root, "system");
405 if (cJSON_IsObject(system)) {
408 summary.imported += r.imported;
409 summary.failed += r.failed;
410 summary.system = true;
411 }
412
413 summary.ok = true;
414 }
415 } else {
416 LOG_E(TAG, "JSON parse failed");
417 }
418 cJSON_Delete(root);
419 }
420
421 mbedtls_platform_zeroize(plain.get(), MAX_PAYLOAD + 1);
422 LOG_I(TAG, "Import done: %u imported, %u failed, %u modules, %u skipped",
423 summary.imported, summary.failed, summary.modules, summary.skipped);
424 return summary;
425}
426
428 struct stat st;
429 return stat(backupPath(), &st) == 0;
430}
431
433 if (!backupExists()) return true;
434 return std::remove(backupPath()) == 0;
435}
436
437} // namespace cdc::os_ui
static const char * TAG
Shared AES-256-GCM helpers built on mbedTLS.
static constexpr size_t NONCE_SIZE
static constexpr size_t TAG_SIZE
static constexpr size_t MAGIC_SIZE
Mounts the FAT-FS partition that holds plugin .wasm + .meta files.
RAII scope that routes cJSON allocations to PSRAM.
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
Module interface that extends IService with module-specific features.
Definition IModule.h:55
virtual bool exportBackup(cJSON *out)
Exports this module's data as a JSON section for the backup file.
Definition IModule.h:100
virtual BackupResult importBackup(const cJSON *in)
Restores this module's data from its JSON backup section.
Definition IModule.h:112
virtual const char * getName() const =0
static ModuleRegistry & instance()
Returns the singleton module registry instance.
bool exportTo(const char *passphrase)
Exports all module sections into one encrypted backup file.
BackupSummary importFrom(const char *passphrase)
Restores from the on-device backup file (best-effort).
bool backupExists() const
Reports whether a backup file is present on the device.
bool deleteBackup()
Deletes the on-device backup file.
static BackupManager & instance()
Returns the process-wide singleton.
static bool exportSystemSettings(cJSON *out)
Writes the user-configurable NVS settings into out.
static cdc::core::IModule::BackupResult importSystemSettings(const cJSON *in)
Restores the system settings from in best-effort.
static const char * basePath()
Returns the VFS path prefix, e.g. "/plugins".
CDC Badge OS plugin host API - canonical C ABI contract.
#define HOST_API_LEVEL_STR
Definition host_api.h:30
#define HOST_API_LEVEL_PACKED
Definition host_api.h:31
#define APP_VERSION
bool aesGcm256Seal(const uint8_t key[32], const uint8_t *iv, size_t ivLen, const uint8_t *aad, size_t aadLen, const uint8_t *pt, size_t ptLen, uint8_t *ctOut, uint8_t tagOut[16])
Encrypts pt with AES-256-GCM and produces a 16-byte tag.
Definition Crypto.h:48
FilePtr openFile(const char *path, const char *mode) noexcept
Open a FILE* and wrap it in a FilePtr.
Definition Raii.h:87
bool aesGcm256Open(const uint8_t key[32], const uint8_t *iv, size_t ivLen, const uint8_t *aad, size_t aadLen, const uint8_t *ct, size_t ctLen, const uint8_t tag[16], uint8_t *ptOut)
Authenticates and decrypts ct with AES-256-GCM.
Definition Crypto.h:79
PsramUniquePtr< T > psramAlloc(std::size_t count) noexcept
Allocate count elements of T in PSRAM (8-bit capable region).
Definition Raii.h:51
Per-module restore outcome reported by importBackup().
Definition IModule.h:85
uint16_t failed
Records skipped due to errors.
Definition IModule.h:87
uint16_t imported
Records restored successfully.
Definition IModule.h:86
Aggregated outcome of a restore across all modules.
uint16_t failed
Total records skipped due to errors.
bool ok
Container decrypted and parsed successfully.
bool system
System/NVS settings section was present and applied.
uint8_t modules
Number of module sections that were applied.
uint16_t imported
Total records restored (modules + system section).
uint8_t skipped
Module sections with no matching module on-device.
Routes cJSON allocations to PSRAM for the lifetime of the scope.
Definition PsramCjson.h:27