Files
story/firmware/echoear-platform-agent/main/ble_cloud.c
T

241 lines
14 KiB
C
Raw Normal View History

#include "ble_cloud.h"
#include "call_client.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include "cJSON.h"
#include "esp_crt_bundle.h"
#include "esp_random.h"
#include "esp_timer.h"
#include "mbedtls/base64.h"
#include "mbedtls/md.h"
#include "mbedtls/sha256.h"
#include "mbedtls/platform_util.h"
#include "nvs.h"
#include "nvs_flash.h"
static char factory_id[18], model[33], origin[160];
static uint8_t device_key[32];
static uint32_t key_version;
static bool configured;
static char voice_boot[37], time_challenge[37];
const char *ble_cloud_origin(void) { return origin; }
const char *ble_cloud_boot_id(void) { if (!voice_boot[0]) ble_random_uuid(voice_boot); return voice_boot; }
const char *ble_cloud_challenge_id(void) { return time_challenge; }
typedef struct { char data[4097]; size_t len; } response_t;
static esp_err_t response_event(esp_http_client_event_t *event)
{
response_t *r = event->user_data;
if (event->event_id == HTTP_EVENT_ON_DATA && event->data_len > 0) {
if (r->len + event->data_len >= sizeof(r->data)) return ESP_ERR_INVALID_SIZE;
memcpy(r->data + r->len, event->data, event->data_len);
r->len += event->data_len;
r->data[r->len] = 0;
}
return ESP_OK;
}
static const char *string(cJSON *obj, const char *key)
{
cJSON *v = cJSON_GetObjectItemCaseSensitive(obj, key);
return cJSON_IsString(v) ? v->valuestring : "";
}
static esp_err_t getstr(nvs_handle_t nvs, const char *key, char *out, size_t len)
{ return nvs_get_str(nvs, key, out, &len); }
esp_err_t ble_cloud_init(void)
{
if (configured) return ESP_OK;
esp_err_t err = nvs_flash_init_partition("ble_factory");
if (err != ESP_OK) return err; // Never erase factory credentials on an init error.
nvs_handle_t nvs;
err = nvs_open_from_partition("ble_factory", "identity", NVS_READONLY, &nvs);
if (err != ESP_OK) return err;
size_t len = sizeof(device_key);
err = getstr(nvs,"factory_id",factory_id,sizeof(factory_id));
if (err == ESP_OK) err = getstr(nvs,"model",model,sizeof(model));
if (err == ESP_OK) err = getstr(nvs,"origin",origin,sizeof(origin));
if (err == ESP_OK) err = nvs_get_blob(nvs,"device_key",device_key,&len);
if (err == ESP_OK && len != 32) err = ESP_ERR_INVALID_SIZE;
if (err == ESP_OK) err = nvs_get_u32(nvs,"key_version",&key_version);
nvs_close(nvs);
if (err != ESP_OK || strlen(factory_id) != 17 || strncmp(factory_id,"KP01-",5) ||
strspn(factory_id+5,"0123456789ABCDEF") != 12 || !model[0] || !key_version ||
strncmp(origin,"https://",8) || strchr(origin+8,'/') || strchr(origin,'@') ||
strpbrk(origin,"\r\n?#")) { mbedtls_platform_zeroize(device_key,32); return ESP_ERR_INVALID_ARG; }
// Move this project's old server address without rewriting factory keys.
if (!strcmp(origin,"https://121.41.46.238"))
snprintf(origin,sizeof(origin),"https://82.157.201.114");
configured = true;
return ESP_OK;
}
bool ble_cloud_configured(void) { return configured; }
const char *ble_factory_id(void) { return factory_id; }
const char *ble_factory_model(void) { return model; }
esp_err_t ble_factory_security(uint8_t salt[16], uint8_t verifier[384])
{
nvs_handle_t nvs;
esp_err_t err = nvs_open_from_partition("ble_factory","identity",NVS_READONLY,&nvs);
if (err != ESP_OK) return err;
size_t len = 16;
err = nvs_get_blob(nvs,"salt",salt,&len);
if (err == ESP_OK && len != 16) err = ESP_ERR_INVALID_SIZE;
len = 384;
if (err == ESP_OK) err = nvs_get_blob(nvs,"verifier",verifier,&len);
if (err == ESP_OK && len != 384) err = ESP_ERR_INVALID_SIZE;
nvs_close(nvs);
return err;
}
void ble_random_uuid(char out[37])
{
uint8_t b[16]; esp_fill_random(b,16); b[6]=(b[6]&15)|64; b[8]=(b[8]&63)|128;
snprintf(out,37,"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],b[8],b[9],b[10],b[11],b[12],b[13],b[14],b[15]);
}
static void hex(const uint8_t *in, size_t n, char *out)
{ for(size_t i=0;i<n;i++) snprintf(out+i*2,3,"%02x",in[i]); }
static void mac(const char *input, char out[65])
{
uint8_t h[32];
mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256),device_key,32,(const uint8_t*)input,strlen(input),h);
hex(h,32,out); mbedtls_platform_zeroize(h,32);
}
static void nonce(char out[23])
{
uint8_t b[16], encoded[25]; size_t len;
esp_fill_random(b,16); mbedtls_base64_encode(encoded,sizeof(encoded),&len,b,16);
for(size_t i=0;i<22;i++) out[i]=encoded[i]=='+'?'-':encoded[i]=='/'?'_':encoded[i];
out[22]=0;
}
esp_err_t ble_cloud_bootstrap(const char *client_nonce, char **response, char password[65])
{
if(!configured||!client_nonce||strlen(client_nonce)!=22||strspn(client_nonce,"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")!=22)return ESP_ERR_INVALID_ARG;
char server_nonce[23],transcript[256],proof[65];nonce(server_nonce);
snprintf(transcript,sizeof(transcript),"BLE-PROOF1\n%s\n%s\n%lu\n%s\n%s",factory_id,model,(unsigned long)key_version,client_nonce,server_nonce);
mac(transcript,proof);
snprintf(transcript,sizeof(transcript),"BLE-PASS1\n%s\n%s\n%lu\n%s\n%s",factory_id,model,(unsigned long)key_version,client_nonce,server_nonce);
mac(transcript,password);
cJSON *out=cJSON_CreateObject();if(!out)return ESP_ERR_NO_MEM;
cJSON_AddStringToObject(out,"device_id",factory_id);cJSON_AddStringToObject(out,"model",model);
cJSON_AddNumberToObject(out,"key_version",key_version);
cJSON_AddStringToObject(out,"client_nonce",client_nonce);cJSON_AddStringToObject(out,"server_nonce",server_nonce);cJSON_AddStringToObject(out,"proof",proof);
*response=cJSON_PrintUnformatted(out);cJSON_Delete(out);
mbedtls_platform_zeroize(transcript,sizeof(transcript));
return *response?ESP_OK:ESP_ERR_NO_MEM;
}
esp_err_t ble_cloud_headers(esp_http_client_handle_t client,const char *method,const char *path,const char *body,const char *request_id)
{
if (!configured) return ESP_ERR_INVALID_STATE;
char timestamp[24], version[16], random[23], digest[65], signature[65], auth[86], canonical[768];
struct timeval tv; gettimeofday(&tv,NULL);
snprintf(timestamp,sizeof(timestamp),"%lld",(long long)tv.tv_sec*1000+tv.tv_usec/1000);
snprintf(version,sizeof(version),"%lu",(unsigned long)key_version);
nonce(random); uint8_t hash[32]; mbedtls_sha256((const uint8_t*)body,strlen(body),hash,0); hex(hash,32,digest);
int n=snprintf(canonical,sizeof(canonical),"DEVREQ1\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s",method,path,factory_id,version,timestamp,random,request_id,digest);
if(n<0 || n>=sizeof(canonical)) return ESP_ERR_INVALID_SIZE;
mac(canonical,signature); snprintf(auth,sizeof(auth),"Device-HMAC-SHA256 %s",signature);
esp_err_t err=esp_http_client_set_header(client,"X-Device-ID",factory_id);
if(err==ESP_OK) err=esp_http_client_set_header(client,"X-Key-Version",version);
if(err==ESP_OK) err=esp_http_client_set_header(client,"X-Timestamp-Ms",timestamp);
if(err==ESP_OK) err=esp_http_client_set_header(client,"X-Nonce",random);
if(err==ESP_OK) err=esp_http_client_set_header(client,"X-Request-ID",request_id);
if(err==ESP_OK) err=esp_http_client_set_header(client,"Authorization",auth);
return err;
}
static esp_err_t post(const char *path,const char *body,const char *request_id,response_t *out,int *status,bool signed_request)
{
char url[256];
// TIME1 is the sole HTTP endpoint: its response is bound to a fresh nonce
// and authenticated with the factory key. Every credential-bearing call is HTTPS.
if(!signed_request && !strcmp(path,"/api/v1/device/time-challenges"))snprintf(url,sizeof(url),"http://%s%s",origin+8,path);
else snprintf(url,sizeof(url),"%s%s",origin,path);
esp_http_client_config_t cfg={.url=url,.timeout_ms=!strcmp(path,"/api/v1/device/ble-bind")?45000:15000,.crt_bundle_attach=esp_crt_bundle_attach,
.event_handler=response_event,.user_data=out,.disable_auto_redirect=true,.buffer_size=1024,.buffer_size_tx=1536};
esp_http_client_handle_t client=esp_http_client_init(&cfg);
if(!client) return ESP_ERR_NO_MEM;
esp_http_client_set_method(client,HTTP_METHOD_POST);
esp_http_client_set_header(client,"Content-Type","application/json");
esp_http_client_set_post_field(client,body,strlen(body));
esp_err_t err=signed_request?ble_cloud_headers(client,"POST",path,body,request_id):ESP_OK;
if(err==ESP_OK) err=esp_http_client_perform(client);
*status=esp_http_client_get_status_code(client); esp_http_client_cleanup(client);
return err;
}
esp_err_t ble_cloud_sync_time(void)
{
const char *boot=ble_cloud_boot_id(); char random[23], body[256]; nonce(random);
snprintf(body,sizeof(body),"{\"device_id\":\"%s\",\"key_version\":%lu,\"boot_id\":\"%s\",\"client_nonce\":\"%s\"}",factory_id,(unsigned long)key_version,boot,random);
response_t *r=calloc(1,sizeof(*r)); if(!r) return ESP_ERR_NO_MEM;
int status=0; int64_t start=esp_timer_get_time();
esp_err_t err=post("/api/v1/device/time-challenges",body,NULL,r,&status,false);
if(err!=ESP_OK || status!=200){free(r);return ESP_FAIL;}
cJSON *root=cJSON_Parse(r->data);
const char *payload=string(root,"payload_b64"), *signature=string(root,"mac_hex");
char expected[65], canonical[3600];
if(strlen(payload)>3400 || strlen(signature)!=64){cJSON_Delete(root);free(r);return ESP_FAIL;}
snprintf(canonical,sizeof(canonical),"TIME1\n%s",payload); mac(canonical,expected);
unsigned mismatch=0; for(int i=0;i<64;i++) mismatch|=expected[i]^signature[i];
uint8_t decoded[2600]; char encoded[3500]; size_t len=strlen(payload),decoded_len=0;
memcpy(encoded,payload,len);
for(size_t i=0;i<len;i++){if(encoded[i]=='-')encoded[i]='+';else if(encoded[i]=='_')encoded[i]='/';}
while(len%4) encoded[len++]='=';
if(mismatch || mbedtls_base64_decode(decoded,sizeof(decoded)-1,&decoded_len,(uint8_t*)encoded,len)!=0){cJSON_Delete(root);free(r);return ESP_FAIL;}
decoded[decoded_len]=0; cJSON *p=cJSON_Parse((char*)decoded); cJSON *time=cJSON_GetObjectItemCaseSensitive(p,"server_time_ms");
bool valid=!strcmp(string(p,"device_id"),factory_id)&&!strcmp(string(p,"boot_id"),boot)&&!strcmp(string(p,"client_nonce"),random)&&cJSON_IsNumber(time)&&time->valuedouble>1700000000000.0&&esp_timer_get_time()-start<15000000;
if(valid){int64_t ms=(int64_t)time->valuedouble;struct timeval tv={.tv_sec=ms/1000,.tv_usec=(ms%1000)*1000};settimeofday(&tv,NULL);snprintf(time_challenge,sizeof(time_challenge),"%s",string(p,"challenge_id"));}
cJSON_Delete(p);cJSON_Delete(root);free(r);return valid?ESP_OK:ESP_FAIL;
}
esp_err_t ble_cloud_bind(const char *token,const char *request_id)
{
if(!configured) return ESP_ERR_INVALID_STATE;
if(ble_cloud_sync_time()!=ESP_OK) return ESP_FAIL;
char body[80];snprintf(body,sizeof(body),"{\"bind_token\":\"%s\"}",token);
response_t *r=calloc(1,sizeof(*r));if(!r)return ESP_ERR_NO_MEM;
int status=0;esp_err_t err=post("/api/v1/device/ble-bind",body,request_id,r,&status,true);
if(err==ESP_OK){
cJSON *root=cJSON_Parse(r->data),*data=cJSON_GetObjectItemCaseSensitive(root,"data");
if(status==200&&cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root,"ok"))&&!strcmp(string(data,"status"),"success")) {
err=call_client_save_bind_voice(data);
// Incomplete responses/storage failures retain the same pending
// token for retry; they do not mean the account binding was denied.
if(err!=ESP_OK)err=ESP_FAIL;
}
else if(status>=400&&status<500&&status!=408&&status!=429) err=ESP_ERR_INVALID_RESPONSE;
else err=ESP_FAIL;
cJSON *secret=cJSON_GetObjectItemCaseSensitive(data,"device_secret");
if(cJSON_IsString(secret))mbedtls_platform_zeroize(secret->valuestring,strlen(secret->valuestring));
cJSON_Delete(root);
}
mbedtls_platform_zeroize(r,sizeof(*r));free(r);return err;
}
esp_err_t ble_cloud_check_bound(const char *request_id)
{
if(!configured)return ESP_ERR_INVALID_STATE;
if(ble_cloud_sync_time()!=ESP_OK)return ESP_FAIL;
response_t *r=calloc(1,sizeof(*r));if(!r)return ESP_ERR_NO_MEM;
int status=0;esp_err_t err=post("/api/v1/device/ble-state","{}",request_id,r,&status,true);
if(err==ESP_OK){
cJSON *root=cJSON_Parse(r->data),*data=cJSON_GetObjectItemCaseSensitive(root,"data"),*bound=cJSON_GetObjectItemCaseSensitive(data,"bound");
if(status==200&&cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(root,"ok"))&&cJSON_IsBool(bound))err=cJSON_IsTrue(bound)?ESP_OK:ESP_ERR_INVALID_RESPONSE;
else if(status>=400&&status<500&&status!=408&&status!=429)err=ESP_ERR_INVALID_RESPONSE;
else err=ESP_FAIL;
cJSON_Delete(root);
}
mbedtls_platform_zeroize(r,sizeof(*r));free(r);return err;
}
char *ble_cloud_decode(const char *domain,const char *encoded,const char *signature)
{
if(!configured || !domain || !encoded || !signature || strlen(signature)!=64 || strlen(encoded)>32768 ||
strspn(encoded,"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")!=strlen(encoded))return NULL;
size_t n=strlen(encoded),d=strlen(domain),out_len=0;
char *canonical=malloc(n+d+2),*padded=calloc(1,n+5),*out=calloc(1,n+1);
if(!canonical||!padded||!out){free(canonical);free(padded);free(out);return NULL;}
snprintf(canonical,n+d+2,"%s\n%s",domain,encoded);
char expected[65];mac(canonical,expected);unsigned mismatch=0;
for(int i=0;i<64;i++)mismatch|=expected[i]^signature[i];
memcpy(padded,encoded,n);for(size_t i=0;i<n;i++){if(padded[i]=='-')padded[i]='+';else if(padded[i]=='_')padded[i]='/';}
size_t len=n;while(len%4)padded[len++]='=';
bool ok=!mismatch&&mbedtls_base64_decode((uint8_t*)out,n,&out_len,(uint8_t*)padded,len)==0;
free(canonical);free(padded);if(!ok){free(out);return NULL;}out[out_len]=0;return out;
}