123 lines
7.1 KiB
C
123 lines
7.1 KiB
C
#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 <stdint.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <time.h>
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|