2번 모듈 신규(raspberry_pi4_sht30/): SHT30 온습도 → cafe24, raw-body X-Signature HMAC, 폐쇄망 오프라인 설치(vendor/install), systemd, CRC/포맷 자가점검. firmware sht30_fw wire 포맷과 byte-parity 검증. 1번 모듈 통합(raspberry_pi4/): 누수+카메라를 동일 서버로. 레거시 sha256(key+json) body-sig → X-Signature HMAC 전환, 사진 canonical=device_id|sensor_id|log_id|timestamp. 서버 통합(php/): api/sensor_data.php 누수 이벤트 수신, api/photo_upload.php 신규(HMAC·finfo·서버생성 파일명·nonce 멱등·sensor 결속), photo.php 인증 사진서빙, dashboard.php 누수 패널+사진, sms_send.php 누수 SMS, config.php 태그/요약. DB: sql/migration_add_leak.sql(가산·멱등), schema_sht30.sql 통합 스키마. 보안 하드닝(외부 펜테스트 대비, 확정 CRITICAL/HIGH 0): cron_heartbeat api_key fail-closed, setup_mfa 부트스트랩 토큰 post-enrollment 잠금, var/ PHP 실행차단, HSTS. 문서/테스트: SYSTEM_OVERVIEW.md, security_hardening_test.php 갱신. 참고: 사용자 요청으로 세션 외 기존 미커밋 변경도 함께 포함. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
3.9 KiB
Python
98 lines
3.9 KiB
Python
# =============================================================================
|
|
# 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()
|