#include "device_reset.h" #include "call_client.h" #include "device_ota.h" #include "ble_cloud.h" #include "cJSON.h" #include "nvs.h" #include "mbedtls/platform_util.h" #include #include #include #include #include typedef struct { uint32_t version,phase; char body[512]; } reset_record_t; // phase 1: request persisted, result uncertain; 2: cloud committed, cleanup // pending; 3: local cleanup committed. Never delete this recovery namespace. static esp_err_t load(reset_record_t *r) { memset(r,0,sizeof(*r));nvs_handle_t n; esp_err_t e=nvs_open("factory_reset",NVS_READONLY,&n); if(e==ESP_ERR_NVS_NOT_FOUND)return ESP_OK; if(e!=ESP_OK)return e; size_t size=sizeof(*r);e=nvs_get_blob(n,"record",r,&size);nvs_close(n); if(e==ESP_ERR_NVS_NOT_FOUND){memset(r,0,sizeof(*r));return ESP_OK;} if(e==ESP_OK && (size!=sizeof(*r)||r->version!=1||r->phase<1||r->phase>3||!memchr(r->body,0,sizeof(r->body))))return ESP_ERR_INVALID_STATE; return e; } static esp_err_t save(const reset_record_t *r) { nvs_handle_t n;esp_err_t e=nvs_open("factory_reset",NVS_READWRITE,&n); if(e!=ESP_OK)return e; e=nvs_set_blob(n,"record",r,sizeof(*r));if(e==ESP_OK)e=nvs_commit(n); reset_record_t check;size_t size=sizeof(check); if(e==ESP_OK)e=nvs_get_blob(n,"record",&check,&size); if(e==ESP_OK && (size!=sizeof(check)||memcmp(r,&check,size)))e=ESP_FAIL; nvs_close(n);return e; } static esp_err_t clear_keys(const char *space,const char *const *keys) { nvs_handle_t n;esp_err_t e=nvs_open(space,NVS_READWRITE,&n);if(e!=ESP_OK)return e; if(!keys)e=nvs_erase_all(n); else for(int i=0;keys[i]&&e==ESP_OK;i++) { e=nvs_erase_key(n,keys[i]);if(e==ESP_ERR_NVS_NOT_FOUND)e=ESP_OK; } if(e==ESP_OK)e=nvs_commit(n); nvs_close(n);return e; } static esp_err_t cleanup(reset_record_t *r) { // Keep firmware/OTA metadata and both forms of permanent device identity. const char *const control[]={"speaker","display_name","standby","result","probe",NULL}; const char *const platform[]={"instance_id","product_key","device_name","volc_secret","pending_conv",NULL}; esp_err_t e=clear_keys("voice",NULL); if(e==ESP_OK)e=clear_keys("platform",platform); if(e==ESP_OK)e=clear_keys("control",control); if(e==ESP_OK)e=clear_keys("ble_setup",NULL); if(e==ESP_OK)e=clear_keys("wifi",NULL); if(e==ESP_OK){r->phase=3;e=save(r);} // A cut before this commit repeats cleanup. return e; } esp_err_t device_reset_boot_cleanup(void) { reset_record_t r;esp_err_t e=load(&r); if(e==ESP_OK && r.phase==2){e=save(&r);if(e==ESP_OK)e=cleanup(&r);} return e; } static void wipe(char *s){if(s){mbedtls_platform_zeroize(s,strlen(s));free(s);}} static esp_err_t post(const char *path,const char *body,cJSON **data) { char *raw=NULL;esp_err_t e=platform_request("POST",path,body,NULL,&raw); if(e==ESP_OK) { cJSON *root=raw?cJSON_Parse(raw):NULL,*value=cJSON_GetObjectItemCaseSensitive(root,"data"); if(!cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root,"ok"))||!cJSON_IsObject(value))e=ESP_ERR_INVALID_RESPONSE; else {*data=cJSON_Duplicate(value,true);if(!*data)e=ESP_ERR_NO_MEM;} cJSON_Delete(root); } wipe(raw);return e; } static const char *str(const cJSON *j,const char *key) { cJSON *v=cJSON_GetObjectItemCaseSensitive(j,key);return cJSON_IsString(v)?v->valuestring:""; } static bool response_matches(const cJSON *data,const cJSON *request) { cJSON *actual=cJSON_GetObjectItemCaseSensitive(data,"reset_counter"),*expected=cJSON_GetObjectItemCaseSensitive(request,"reset_counter"); if(strcmp(str(data,"reset_id"),str(request,"reset_id")) || strcmp(str(data,"activation_state"),"CLAIMABLE") || strcmp(str(data,"license_action"),"RETAINED") || !cJSON_IsNumber(actual)||!cJSON_IsNumber(expected)||actual->valuedouble!=expected->valuedouble)return false; const char *required[]={"clear_volc_credential","clear_user_context","clear_session_state","clear_network_credentials","reboot_to_claimable"}; cJSON *actions=cJSON_GetObjectItemCaseSensitive(data,"local_actions"); if(!cJSON_IsArray(actions)||cJSON_GetArraySize(actions)!=5)return false; for(int i=0;i<5;i++) {int count=0;const cJSON *a=NULL;cJSON_ArrayForEach(a,actions)if(cJSON_IsString(a)&&!strcmp(a->valuestring,required[i]))count++;if(count!=1)return false;} return true; } esp_err_t device_reset_from_setup(const char *physical_event_id) { if(!physical_event_id||strlen(physical_event_id)!=36||!ble_cloud_configured())return ESP_ERR_INVALID_ARG; reset_record_t r;esp_err_t e=load(&r);if(e!=ESP_OK)return e; if(r.phase==2){e=save(&r);return e==ESP_OK?cleanup(&r):e;} // Same confirmed BLE job may retry after local cleanup finished. cJSON *request=r.phase?cJSON_Parse(r.body):NULL; if(r.phase==3 && request && !strcmp(str(request,"reset_id"),physical_event_id)){cJSON_Delete(request);return ESP_OK;} cJSON_Delete(request);request=NULL; // BLE setup boots before normal SNTP/call initialization. Establish signed // server time before DEVREQ1 timestamps and the durable occurred_at value. if(ble_cloud_sync_time()!=ESP_OK)return ESP_FAIL; if(r.phase!=1) { cJSON *state=NULL;e=post("/api/v1/device/factory-reset/state","{}",&state); if(e!=ESP_OK){cJSON_Delete(state);return e;} cJSON *counter=cJSON_GetObjectItemCaseSensitive(state,"reset_counter"); if(!cJSON_IsNumber(counter)||counter->valuedouble<0||counter->valuedouble>=9007199254740991.0||counter->valuedouble!=(double)(int64_t)counter->valuedouble){cJSON_Delete(state);return ESP_ERR_INVALID_RESPONSE;} double next=counter->valuedouble+1;cJSON_Delete(state); time_t now=time(NULL);struct tm utc;char occurred[32]; if(now<1700000000 || !gmtime_r(&now,&utc) || !strftime(occurred,sizeof(occurred),"%Y-%m-%dT%H:%M:%SZ",&utc))return ESP_ERR_INVALID_STATE; request=cJSON_CreateObject();if(!request)return ESP_ERR_NO_MEM; cJSON_AddStringToObject(request,"reset_id",physical_event_id);cJSON_AddNumberToObject(request,"reset_counter",next); cJSON_AddStringToObject(request,"occurred_at",occurred);cJSON_AddStringToObject(request,"trigger","device_menu"); cJSON_AddStringToObject(request,"firmware_version",device_ota_version());cJSON_AddNumberToObject(request,"pending_turn_count",0); char *body=cJSON_PrintUnformatted(request);cJSON_Delete(request);request=NULL; if(!body)return ESP_ERR_NO_MEM; if(strlen(body)>=sizeof(r.body)){wipe(body);return ESP_ERR_INVALID_SIZE;} memset(&r,0,sizeof(r));r.version=1;r.phase=1;strcpy(r.body,body);wipe(body); if((e=save(&r))!=ESP_OK)return e; } request=cJSON_Parse(r.body);if(!request)return ESP_ERR_INVALID_STATE; cJSON *response=NULL;e=post("/api/v1/device/factory-reset",r.body,&response); if(e==ESP_OK && !response_matches(response,request))e=ESP_ERR_INVALID_RESPONSE; cJSON_Delete(request);cJSON_Delete(response); if(e!=ESP_OK)return e; // Keep the same signed body for uncertain retries. r.phase=2;if((e=save(&r))!=ESP_OK)return e; return cleanup(&r); }