# 변경 이력 ## Unreleased - 보안 하드닝 (전체 코드리뷰 반영) ### ⚠️ 호환성 주의 (BREAKING) - **요청 서명을 키 접두 SHA256 → HMAC-SHA256 으로 전환했습니다.** 길이확장(length- extension) 공격에 견디기 위함입니다. `X-Signature = HMAC-SHA256(key=API_KEY, msg=raw_body)`. 서버(`verify_signature_raw`)·펌웨어(`sig.c`)·호스트 레퍼런스(`reference.py`)를 동시에 바꿨으므로, **펌웨어 재플래시와 서버 배포를 함께** 진행해야 합니다(구 펌웨어 ↔ 신 서버는 서명 불일치로 거부됨). 호스트 패리티 테스트와 ARM `-Wconversion -Werror` 컴파일로 검증했습니다. ### 서버 (PHP) - `api/sensor_data.php`: 재전송(replay) 방어를 추가했습니다 — 본문 `timestamp` 가 `INGEST_MAX_CLOCK_SKEW_SEC`(기본 600초) 범위를 벗어나거나 0 이하이면 거부합니다. - `api/sensor_data.php`: 운영 비밀값 강제(fail-closed). `API_KEY` 가 예시 기본값이면 수신을 503 으로 거부합니다(`api_key_is_default()`). - `api/sensor_data.php`: 디바이스 문자열(device_id/sensor_name/device_location/metric_type) 의 제어문자를 제거해 SMS/로그 라인 주입을 막습니다. - `cron_heartbeat.php`: 오프라인 SMS 중복 발송 경합을 제거했습니다 — `offline_alerted` 0→1 전이를 원자적 조건부 UPDATE 로 선점한 프로세스만 발송합니다. - `monthly_report.php`: CSV 내보내기의 수식 주입(formula injection)을 방지합니다(자유 텍스트 셀이 `= + - @` 등으로 시작하면 무력화). - `retention_cleanup.php`: 감사로그 회전을 배타 락(flock)으로 묶어 동시 기록 유실을 막습니다. - `login.php`: 브루트포스 카운터를 원자적 읽기-수정-쓰기로 변경해 동시 실패 시 카운트 덮어쓰기(lost update)를 막습니다. - `config.local.example.php`: 예시 TOTP 비밀키를 공개 테스트 값에서 명백히 무효한 플레이스홀더로 교체했습니다(MFA 가 실제 비밀키 설정 전까지 fail-closed). ### DB - `sql/migration_drop_leak.sql`: information_schema 가드 프로시저로 멱등화했습니다(재실행/ 부분 적용 DB 에서도 오류 없이 진행). ### 보안 리뷰 후속 수정 (다중 에이전트 검증에서 발견) - **(HIGH) 예시 TOTP 비밀키 회귀 수정.** `config.local.example.php` 의 `ADMIN_TOTP_SECRET` 플레이스홀더(`REPLACE_WITH_UNIQUE_BASE32_SECRET`)가 Base32 로 디코드되어(`REPLACEWITHUNIQUEBASE32SECRET` → 18바이트 키) **실제 코드 `275426` 를 생성** = 공개된 시드로 MFA 가 활성화되는 문제였습니다. 빈 문자열 `''` 로 교체해 fail-closed(키 미설정 시 로그인 차단)로 만들고, 회귀 테스트로 고정했습니다. - **(MEDIUM) 레거시 서명 다운그레이드 경로 차단.** `api/sensor_data.php` 가 `X-Signature` 헤더가 없으면 약한 키-접두 sha256(`verify_signature`)로 폴백하던 것을, `ALLOW_LEGACY_BODY_SIGNATURE`(기본 false)로 게이트했습니다. 기본값에서는 HMAC 헤더가 없으면 거부합니다(운영 STM32 클라이언트는 HMAC 만 사용). - **(LOW) 인증 전 본문 크기 상한.** `INGEST_MAX_BODY_BYTES`(기본 8KB) 초과 본문은 json_decode/HMAC 이전에 413 으로 거부 — 미인증 증폭형 DoS 차단. - **(LOW) heartbeat throttle 원자화.** `check_offline_sensors_throttled()` 의 비원자 read-then-write 를 flock 기반 원자 판정으로 교체 — 동시 호출 시 중복 오프라인 스캔 방지. - **(LOW) `CRON_SECRET` 강화.** `md5('cron_'.API_KEY)` → `hash_hmac('sha256','cron',API_KEY)`. - 알려진 잔여(문서화): 600초 창 내 재전송 dedup 부재, 운영 키 보유자에 의한 ingest flooding(호스트 레벨 rate-limit/WAF 권장), `.htaccess` Apache 전용. ## Unreleased - SHT30 온습도 전용 전환 ### 시스템 범위 - 시스템을 서버실 **온습도(SHT30) 24시간 모니터링 + 임계 초과 SMS + 웹 대시보드** 전용으로 전환했습니다. 누수 감지/카메라/사진/사고대응(incident) 기능은 전면 제거했습니다. - 단일 보드 구성으로 단순화: `sht30_fw`(`sensor_id=2`, `device_id=stm32-sht30-01`)만 사용합니다. 누수 보드(`board_leak`/`leak_fw`)는 제거했습니다. ### 서버 (PHP/DB) - 온습도 임계 SMS를 추가했습니다: 고온 30℃ / 저온 10℃ / 고습 70% / 저습 20%, 복귀 히스테리시스 ±1℃·±3%, 동일 종류 쿨다운 1800초(30분). 임계는 서버 판정값(`php/config.php` `METRIC_*`, `config.local.php` override)이라 폐쇄망에서 펌웨어 재플래시 없이 변경 가능합니다. - 측정값을 `sensor_metric`에 저장하고 `metric_status`로 경보/정상복귀를 판정합니다. 장비 오프라인/복구는 `HEARTBEAT_TIMEOUT_SEC`(기본 1200초=20분)으로 판정합니다. - 사진 업로드/열람 경로(`api/photo_upload.php`, `photo.php`)와 사고 대응(`incidents.php`)을 제거했습니다. - 신규 설치용 통합 스키마를 `sql/schema_sht30.sql`로 정리했습니다(테이블: `sensor_log`, `sensor_status`, `sensor_metric`, `sms_log`). 기존 누수 설치 전환용 `sql/migration_drop_leak.sql`을 추가해 레거시 누수 컬럼/테이블(`leak_photo`, `leak_incident`, `is_leak` 등)을 정리합니다. ### 펌웨어 (STM32F407 `sht30_fw`) - 누수 이벤트 본문에서 `is_leak` 와이어 필드를 제거하고 온습도 측정 보고로 단일화했습니다. - SHT30 I2C 배선을 확정했습니다: I2C1 PB6(SCL)/PB7(SDA), 주소 0x44, 3.3V/GND, 4.7kΩ 풀업. 측정 주기 `APP_SHT30_REPORT_INTERVAL_SEC=300`(5분). - 레거시 4.7MΩ 누수 풀다운 회로 및 e-con 5C-08 결선 안내를 폐기했습니다. ### 문서 - README, 설치/운영 문서, 배선도, 정리 보고서를 SHT30 온습도 전용으로 재작성했습니다. 누수/사진/카메라/사고대응 관련 절·표를 온습도 임계 SMS/쿨다운/오프라인 기준으로 치환했습니다. ## Unreleased - 메인 MCU 마이그레이션 (Raspberry Pi → STM32F407VGT6) ### 서버 (PHP) - `sensor_data.php` 인증을 이중 모드로 확장했습니다: `X-Signature` 헤더 기반 raw-body 서명(신규, STM32 펌웨어용)과 기존 본문 필드 서명(RPi, 하위 호환)을 모두 수용합니다. - `config.php`에 `verify_signature_raw()`를 추가했습니다 — `sha256(API_KEY . 요청본문)`로 검증하여 JSON 재직렬화/부동소수 포맷 차이로 인한 서명 불일치(R1)를 제거합니다. - raw-body/레거시 서명 회귀 테스트 `tests/raw_body_signature_test.php`를 추가했습니다. ### 펌웨어 (신규 `firmware/`, STM32F407VGT6 / Cortex-M4F) - 메인 MCU를 Raspberry Pi Zero 2 W(Linux/Python)에서 STM32F407VGT6 베어메탈로 이식했습니다. 구성: 유선 Ethernet(LAN8720 RMII) + LwIP + mbedTLS(HTTPS) + FreeRTOS. WiFi/카메라는 제거. - 두 보드 변형: `BOARD_LEAK`(누수, sensor_id=1), `BOARD_SHT30`(온습도, sensor_id=2). RPi 2대 구성을 미러링. - 이식 가능한 코어(`sha256_sw`/`sig`/`jsonbody`/`sht30_convert`/`httpapi`)는 호스트 패리티(Python↔PHP `verify_signature_raw`) + ARM 크로스컴파일(`-Werror -Wconversion`)로 검증했습니다. - 다중 에이전트 코드리뷰에서 확인된 결함을 수정: TLS 인증서 유효기간 검증 활성화(RTC/SNTP 시간 공급), FreeRTOS 정적메모리 콜백 중복정의 제거, LwIP 코어락 위반(PHY 링크/DNS) 수정, 로그 UART PA2 충돌 → USART3(PD8/PD9) 이동, EXTI 핸들러 추가, HW RNG 기반 `bsp_rand32`. - 폐쇄망 빌드: 의존성은 `firmware/third_party`에 벤더링, 오프라인 빌드/플래시 절차 문서화(`firmware/docs/BUILD_OFFLINE.md`). - `docs/stm32f407_migration_plan.md`(계획+구현 현황), `firmware/docs/{HARDWARE,PORTING_NOTES}.md`를 추가했습니다. - 레거시 RPi 펌웨어(`raspberry_pi/`)와 systemd 유닛(`systemd/`)을 제거했습니다(STM32 펌웨어로 대체). ## v2605 - 2026-05-19 - v2605 정리 전 원본을 `_backup/pre_v2605_20260519/`에 보관했습니다. - PHP/RPi 운영 비밀값을 코드 하드코딩에서 로컬 설정/환경 변수 방식으로 분리했습니다. - `config.local.example.php`, `config.example.py`, `leak-sensor.env.example`을 추가했습니다. - 누수경보 SMS 쿨다운이 실제 발송 로직에 적용되도록 수정했습니다. - 누수 지속 SMS를 시간 기반 5분 정책으로 정리했습니다. - heartbeat 체크 주기와 오프라인 판정 시간을 분리했습니다. - 사진 업로드가 실제 `leak_alert` 로그 ID를 검증하도록 강화했습니다. - 신규 설치용 통합 DB 스키마 `sql/schema_v2605.sql`을 추가했습니다. - README와 보고서를 실제 코드 기준으로 최신화했습니다. - 누수 사고 대응 테이블과 대시보드 사고 패널을 추가했습니다. - 누수 사고 `확인함`, `출동 중`, `조치 완료`, `오탐` 상태 변경을 지원합니다. - 미확인 누수 사고 SMS 에스컬레이션을 추가했습니다. - 대시보드 첫 화면을 위험 상태 우선 구조로 재배치했습니다. - 운영 자가진단 패널을 추가해 설정값, 마이그레이션, 권한, SMS 실패를 표시합니다. - 30일 운영 요약을 추가해 사고 상태, 평균 확인/조치 시간, SMS 실패, 사진 저장 현황을 보여줍니다. - 설치/운영 점검 화면 `php/setup_wizard.php`를 추가했습니다. - 보안대책서 첨부용 증적 보고서 `php/security_evidence.php`와 운영·보안 체크리스트를 추가했습니다. - 월간 보고서 화면 `php/monthly_report.php`를 추가했습니다. - 누수 사진 타임라인 다중 촬영을 추가했습니다. - 사진 타임라인 DB 컬럼과 기존 설치용 `sql/migration_photo_timeline.sql`을 추가했습니다. - Pi 증적 수집 스크립트를 추가해 API 키 원문 없이 서비스/포트/환경파일 상태를 확인할 수 있게 했습니다.