# ============================================================================= # wire.py - 서버 전송용 wire 포맷 (본문 JSON + raw-body HMAC 서명) # # [왜 별도 모듈인가] # 서버(php/api/sensor_data.php)는 받은 "본문 바이트 그대로"에 대해 # X-Signature = lowercase_hex(HMAC-SHA256(key=API_KEY, msg=raw_body)) 를 검증한다. # 따라서 "서명한 바이트"와 "실제 전송한 바이트"가 100% 동일해야 한다. # 본문을 직접 문자열로 만들고 그 바이트에 서명한 뒤, 그 바이트를 그대로 POST 한다. # (requests 의 json= 직렬화에 맡기면 부동소수/공백/순서가 달라져 서명이 깨질 수 있다.) # # [바이트 단위 호환 대상] — 아래 넷과 출력이 동일해야 한다. # - firmware/common/jsonbody.c::jb_sht30_event (STM32 본문 생성) # - firmware/common/sig.c::sig_raw_body (STM32 HMAC 서명) # - firmware/test/host/reference.py (검증된 Python 레퍼런스) # - php/config.php::verify_signature_raw (서버 검증부) # # fixed2() <-> jb_fixed2() / reference.fixed2() # sht30_event_body() <-> jb_sht30_event() / reference.sht30_event_body() # sign_raw() <-> sig_raw_body() / reference.sign_raw() # ============================================================================= from __future__ import annotations import hashlib import hmac def fixed2(v: float) -> str: """소수 2자리 고정 포맷 (round-half-up, locale 무관). jb_fixed2() / reference.fixed2() 와 바이트 동일. 24.0 -> "24.00" 48.5 -> "48.50" -3.245 -> "-3.25" """ neg = v < 0 if neg: v = -v scaled = int(v * 100.0 + 0.5) # round half up (C: (long long)(v*100+0.5)) ip = scaled // 100 fp = scaled % 100 sign = "-" if (neg and scaled != 0) else "" return f"{sign}{ip}.{fp:02d}" def _esc(s: str) -> str: r"""JSON 문자열 값 이스케이프 (jb_estr / reference._esc 와 동일). '"' 와 '\\' 와 제어문자(<0x20)만 이스케이프한다. '/' 는 그대로 둔다. 멀티바이트 UTF-8(한글 등)은 그대로 둔다(서버가 raw 바이트로 검증). """ out = [] for ch in s: o = ord(ch) if ch == '"': out.append('\\"') elif ch == '\\': out.append('\\\\') elif o < 0x20: out.append('\\u%04x' % o) else: out.append(ch) return "".join(out) def sht30_event_body(device_id: str, device_location: str, sensor_id: int, sensor_name: str, event_type: str, timestamp: int, temperature_c: float, humidity_percent: float, metric_status: str, app_version: str) -> str: """SHT30 이벤트 본문 JSON 문자열 생성. 필드 순서는 펌웨어 jb_sht30_event() 와 동일하게 고정한다(서명 호환). metric_type 은 항상 "sht30" 고정. 온도/습도는 fixed2 로 2자리 직렬화. """ return ( '{' f'"device_id":"{_esc(device_id)}",' f'"device_location":"{_esc(device_location)}",' f'"sensor_id":{int(sensor_id)},' f'"sensor_name":"{_esc(sensor_name)}",' f'"event_type":"{_esc(event_type)}",' f'"timestamp":{int(timestamp)},' f'"metric_type":"sht30",' f'"temperature_c":{fixed2(temperature_c)},' f'"humidity_percent":{fixed2(humidity_percent)},' f'"metric_status":"{_esc(metric_status)}",' f'"app_version":"{_esc(app_version)}"' '}' ) def sign_raw(api_key: str, body: str) -> str: """raw-body 서명: lowercase_hex(HMAC-SHA256(key=api_key, msg=body)). sig_raw_body() / reference.sign_raw() / php verify_signature_raw 와 동일. 본문은 UTF-8 로 인코딩한 뒤 서명한다(전송 바이트와 동일). """ return hmac.new(api_key.encode("utf-8"), body.encode("utf-8"), hashlib.sha256).hexdigest()