Compare commits
6 Commits
a585a56cff
...
8342904488
| Author | SHA1 | Date | |
|---|---|---|---|
| 8342904488 | |||
| ef00afb14e | |||
| 96ede7c999 | |||
| e2dbe6a2d5 | |||
| 2226c1b4ca | |||
| a0eee0e6d4 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,9 @@
|
|||||||
.worktrees/
|
.worktrees/
|
||||||
.agent/
|
.agent/
|
||||||
.claude/
|
.claude/
|
||||||
|
.adversarial-review/
|
||||||
graphify-out/
|
graphify-out/
|
||||||
firmware/.pio/
|
firmware/.pio/
|
||||||
*.log
|
*.log
|
||||||
|
*secret*
|
||||||
|
__pycache__/
|
||||||
|
|||||||
@@ -14,12 +14,21 @@ static HString bytes_to_hex(const uint8_t* bytes, size_t len) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void hex_to_bytes(const HString& hex, uint8_t* out, size_t out_len) {
|
static bool is_hex_char(char c) {
|
||||||
if (hex.length() % 2 != 0) return; // malformed — odd-length hex
|
return (c >= '0' && c <= '9') ||
|
||||||
for (size_t i = 0; i < out_len && (i * 2 + 1) < hex.length(); i++) {
|
(c >= 'a' && c <= 'f') ||
|
||||||
char byte_str[3] = {hex[i*2], hex[i*2+1], 0};
|
(c >= 'A' && c <= 'F');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool hex_to_bytes(const HString& hex, uint8_t* out, size_t out_len) {
|
||||||
|
if (hex.length() != out_len * 2) return false;
|
||||||
|
for (size_t i = 0; i < out_len; i++) {
|
||||||
|
char a = hex[i*2], b = hex[i*2+1];
|
||||||
|
if (!is_hex_char(a) || !is_hex_char(b)) return false;
|
||||||
|
char byte_str[3] = {a, b, 0};
|
||||||
out[i] = (uint8_t)strtol(byte_str, nullptr, 16);
|
out[i] = (uint8_t)strtol(byte_str, nullptr, 16);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool sha256(const uint8_t* data, size_t len, uint8_t out[32]) {
|
static bool sha256(const uint8_t* data, size_t len, uint8_t out[32]) {
|
||||||
@@ -52,10 +61,20 @@ HString hmac_sign(const HString& secret_hex,
|
|||||||
snprintf(ts_buf, sizeof(ts_buf), "%u", (unsigned)timestamp);
|
snprintf(ts_buf, sizeof(ts_buf), "%u", (unsigned)timestamp);
|
||||||
HString message = method + "\n" + path + "\n" + ts_buf + "\n" + body_hash_hex;
|
HString message = method + "\n" + path + "\n" + ts_buf + "\n" + body_hash_hex;
|
||||||
|
|
||||||
// 3. Decode secret from hex
|
// 3. Decode secret from hex. Reject empty / odd-length / oversized /
|
||||||
|
// non-hex inputs — flash_device.py validates at provision time, but
|
||||||
|
// hmac_sign refuses to sign under a malformed key regardless of how it
|
||||||
|
// ended up in NVS (legacy provisioning, NVS corruption, etc.).
|
||||||
|
if (secret_hex.length() == 0 ||
|
||||||
|
secret_hex.length() > 128 ||
|
||||||
|
secret_hex.length() % 2 != 0) {
|
||||||
|
return HString{};
|
||||||
|
}
|
||||||
size_t secret_len = secret_hex.length() / 2;
|
size_t secret_len = secret_hex.length() / 2;
|
||||||
uint8_t secret[64] = {};
|
uint8_t secret[64] = {};
|
||||||
hex_to_bytes(secret_hex, secret, secret_len);
|
if (!hex_to_bytes(secret_hex, secret, secret_len)) {
|
||||||
|
return HString{};
|
||||||
|
}
|
||||||
|
|
||||||
// 4. HMAC-SHA256(secret, message)
|
// 4. HMAC-SHA256(secret, message)
|
||||||
uint8_t hmac_result[32];
|
uint8_t hmac_result[32];
|
||||||
|
|||||||
@@ -126,7 +126,11 @@ extern "C" void net_guard_tick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (s_up || s_cfg == nullptr) return;
|
if (s_up || s_cfg == nullptr) return;
|
||||||
if (millis() < s_next_retry_ms) return;
|
// Wrap-safe: signed difference handles the ~49.7-day millis() wrap. The
|
||||||
|
// device is meant to run for months between reboots, so absolute compare
|
||||||
|
// (millis() < s_next_retry_ms) would either tight-loop retries across the
|
||||||
|
// wrap or stall them until millis() climbed back past an old high mark.
|
||||||
|
if ((int32_t)(millis() - s_next_retry_ms) < 0) return;
|
||||||
if (s_up) return; // re-check after the timing gate — closes GOT_IP-vs-tick race
|
if (s_up) return; // re-check after the timing gate — closes GOT_IP-vs-tick race
|
||||||
s_attempts++;
|
s_attempts++;
|
||||||
// WiFi.begin() alone re-associates cleanly; a prior WiFi.disconnect() call
|
// WiFi.begin() alone re-associates cleanly; a prior WiFi.disconnect() call
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <freertos/semphr.h>
|
#include <freertos/semphr.h>
|
||||||
@@ -296,7 +297,10 @@ void reporter_flush(const DeviceConfig& cfg) {
|
|||||||
String body = build_camera_batch(cfg, cam_snap);
|
String body = build_camera_batch(cfg, cam_snap);
|
||||||
if (post_json(cfg, "/api/v1/camera/events/batch", body)) {
|
if (post_json(cfg, "/api/v1/camera/events/batch", body)) {
|
||||||
xSemaphoreTake(s_buf_mutex, portMAX_DELAY);
|
xSemaphoreTake(s_buf_mutex, portMAX_DELAY);
|
||||||
s_cam_buf.clear();
|
// Erase only the prefix we snapshotted; FIFO append from
|
||||||
|
// submit_camera during the in-flight POST stays buffered.
|
||||||
|
size_t n = std::min(cam_snap.size(), s_cam_buf.size());
|
||||||
|
s_cam_buf.erase(s_cam_buf.begin(), s_cam_buf.begin() + n);
|
||||||
xSemaphoreGive(s_buf_mutex);
|
xSemaphoreGive(s_buf_mutex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,7 +308,8 @@ void reporter_flush(const DeviceConfig& cfg) {
|
|||||||
String body = build_ble_batch(cfg, ble_snap);
|
String body = build_ble_batch(cfg, ble_snap);
|
||||||
if (post_json(cfg, "/api/v1/events/batch", body)) {
|
if (post_json(cfg, "/api/v1/events/batch", body)) {
|
||||||
xSemaphoreTake(s_buf_mutex, portMAX_DELAY);
|
xSemaphoreTake(s_buf_mutex, portMAX_DELAY);
|
||||||
s_ble_buf.clear();
|
size_t n = std::min(ble_snap.size(), s_ble_buf.size());
|
||||||
|
s_ble_buf.erase(s_ble_buf.begin(), s_ble_buf.begin() + n);
|
||||||
xSemaphoreGive(s_buf_mutex);
|
xSemaphoreGive(s_buf_mutex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,13 +70,15 @@ def store_heartbeat_diagnostics(
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
cursor = db.cursor()
|
cursor = db.cursor()
|
||||||
|
# COALESCE preserves existing column values when the v1.0.0 payload omits
|
||||||
|
# diagnostic fields (Pydantic resolves them to None).
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""UPDATE heartbeats
|
"""UPDATE heartbeats
|
||||||
SET reset_reason = ?,
|
SET reset_reason = COALESCE(?, reset_reason),
|
||||||
heap_free = ?,
|
heap_free = COALESCE(?, heap_free),
|
||||||
heap_min_free = ?,
|
heap_min_free = COALESCE(?, heap_min_free),
|
||||||
last_disconnect_code = ?,
|
last_disconnect_code = COALESCE(?, last_disconnect_code),
|
||||||
recent_events = ?
|
recent_events = COALESCE(?, recent_events)
|
||||||
WHERE device_id = ?""",
|
WHERE device_id = ?""",
|
||||||
(
|
(
|
||||||
hb.reset_reason,
|
hb.reset_reason,
|
||||||
|
|||||||
@@ -15,11 +15,14 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
HMAC_SECRET_RE = re.compile(r"^[0-9a-fA-F]{64}$")
|
||||||
|
|
||||||
|
|
||||||
NVS_NAMESPACE = "doorcounter"
|
NVS_NAMESPACE = "doorcounter"
|
||||||
NVS_PARTITION_OFFSET = "0x9000"
|
NVS_PARTITION_OFFSET = "0x9000"
|
||||||
@@ -63,6 +66,10 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
hmac_secret = args.hmac_secret or secrets.token_hex(32)
|
hmac_secret = args.hmac_secret or secrets.token_hex(32)
|
||||||
|
if not HMAC_SECRET_RE.match(hmac_secret):
|
||||||
|
print("Error: --hmac-secret must be exactly 64 hex characters (32 bytes)",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
if args.hmac_secret is None:
|
if args.hmac_secret is None:
|
||||||
print(f"Generated HMAC secret: {hmac_secret}")
|
print(f"Generated HMAC secret: {hmac_secret}")
|
||||||
print(" *** SAVE THIS — you need it to register the device on the server ***")
|
print(" *** SAVE THIS — you need it to register the device on the server ***")
|
||||||
|
|||||||
Reference in New Issue
Block a user