241 lines
14 KiB
C
241 lines
14 KiB
C
#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;
|
||
|
|
}
|