From 65b42df1824ab8156060e6800967bb437f2ecb29 Mon Sep 17 00:00:00 2001 From: jhchun Date: Fri, 19 Jun 2026 11:08:19 +0900 Subject: [PATCH] =?UTF-8?q?BLE=20=EB=B3=B4=EC=95=88,=20=ED=8C=A8=EC=8A=A4?= =?UTF-8?q?=ED=82=A4=20=EB=93=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prj.conf | 9 +- src/app_nvs.c | 49 ++++---- src/ble/ble_service.c | 263 ++++++++++++++++++++++++++++++++++++++++-- src/ble/ble_service.h | 4 +- 4 files changed, 292 insertions(+), 33 deletions(-) diff --git a/prj.conf b/prj.conf index 516da9d..83d4f07 100644 --- a/prj.conf +++ b/prj.conf @@ -43,8 +43,13 @@ CONFIG_BT_PERIPHERAL_PREF_MAX_INT=24 CONFIG_BT_PERIPHERAL_PREF_LATENCY=0 CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400 -# Security (dev mode - disabled for now) -CONFIG_BT_SMP=n +# BLE security (production) +CONFIG_BT_SMP=y +CONFIG_BT_BONDABLE=y +CONFIG_BT_APP_PASSKEY=y +CONFIG_BT_SMP_ENFORCE_MITM=y +CONFIG_BT_SMP_SC_ONLY=y +CONFIG_BT_SETTINGS=y # ADC (battery, temperature) CONFIG_ADC=y diff --git a/src/app_nvs.c b/src/app_nvs.c index 318514b..d85c301 100644 --- a/src/app_nvs.c +++ b/src/app_nvs.c @@ -14,26 +14,31 @@ #include "main.h" #include "debug_print.h" +/* Zephyr settings에서 사용할 저장소 이름 */ #define APP_NVS_SETTINGS_NAME "vesiscan" + +/* vesiscan 저장소 안에서 쓰는 짧은 키 */ #define APP_NVS_KEY_PIEZO "piezo" #define APP_NVS_KEY_HW_NO "hw" #define APP_NVS_KEY_SERIAL_NO "serial" #define APP_NVS_KEY_PASSKEY "passkey" -#define APP_NVS_KEY_PASSKEY_CHANGED "passkey_changed" /* 패스키 1회 변경 완료 플래그 */ +#define APP_NVS_KEY_PASSKEY_CHANGED "passkey_changed" /* 패스키 1회 변경 완료 여부 */ -#define APP_NVS_FULL_KEY_PIEZO "vesiscan/piezo" -#define APP_NVS_FULL_KEY_HW_NO "vesiscan/hw" -#define APP_NVS_FULL_KEY_SERIAL_NO "vesiscan/serial" -#define APP_NVS_FULL_KEY_PASSKEY "vesiscan/passkey" -#define APP_NVS_FULL_KEY_PASSKEY_CHANGED "vesiscan/passkey_changed" /* 값 0은 변경 가능, 값 1은 변경 차단 */ +/* settings_save_one에 넘기는 전체 키 */ +#define APP_NVS_FULL_KEY_PIEZO "vesiscan/piezo" +#define APP_NVS_FULL_KEY_HW_NO "vesiscan/hw" +#define APP_NVS_FULL_KEY_SERIAL_NO "vesiscan/serial" +#define APP_NVS_FULL_KEY_PASSKEY "vesiscan/passkey" +#define APP_NVS_FULL_KEY_PASSKEY_CHANGED "vesiscan/passkey_changed" /* 0은 변경 가능, 1은 변경 차단 */ +/* parser.c가 가진 피에조 설정을 NVS 로드 시 갱신하기 위한 포인터 */ static piezo_config_t *m_piezo_cfg; static app_nvs_piezo_validate_fn m_piezo_validate; static app_nvs_piezo_log_fn m_piezo_log; #if IS_ENABLED(CONFIG_SETTINGS) -static int read_exact(settings_read_cb read_cb, void *cb_arg, - void *dst, size_t expected_len, const char *key) +/* 고정 길이 설정값을 정확한 크기로 읽기 */ +static int read_exact(settings_read_cb read_cb, void *cb_arg, void *dst, size_t expected_len, const char *key) { int rc = read_cb(cb_arg, dst, expected_len); @@ -51,11 +56,11 @@ static int read_exact(settings_read_cb read_cb, void *cb_arg, return 0; } -static int app_nvs_settings_set(const char *name, size_t len, - settings_read_cb read_cb, void *cb_arg) +static int app_nvs_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { const char *next; + /* 피에조 설정은 검증을 통과한 값만 기본값 대신 적용 */ if (settings_name_steq(name, APP_NVS_KEY_PIEZO, &next) && (next == NULL)) { piezo_config_t loaded; @@ -88,6 +93,7 @@ static int app_nvs_settings_set(const char *name, size_t len, return 0; } + /* HW Version은 NUL 종료 없이 고정 길이 ASCII로 저장 */ if (settings_name_steq(name, APP_NVS_KEY_HW_NO, &next) && (next == NULL)) { if (len != HW_NO_LENGTH) @@ -100,6 +106,7 @@ static int app_nvs_settings_set(const char *name, size_t len, return read_exact(read_cb, cb_arg, HW_NO, HW_NO_LENGTH, APP_NVS_KEY_HW_NO); } + /* Serial Number는 NUL 종료 없이 고정 길이 ASCII로 저장 */ if (settings_name_steq(name, APP_NVS_KEY_SERIAL_NO, &next) && (next == NULL)) { if (len != SERIAL_NO_LENGTH) @@ -112,6 +119,7 @@ static int app_nvs_settings_set(const char *name, size_t len, return read_exact(read_cb, cb_arg, SERIAL_NO, SERIAL_NO_LENGTH, APP_NVS_KEY_SERIAL_NO); } + /* 패스키는 mpz? 명령으로 최초 1회만 저장 */ if (settings_name_steq(name, APP_NVS_KEY_PASSKEY, &next) && (next == NULL)) { if (len != PASSKEY_LENGTH) @@ -126,7 +134,7 @@ static int app_nvs_settings_set(const char *name, size_t len, if (settings_name_steq(name, APP_NVS_KEY_PASSKEY_CHANGED, &next) && (next == NULL)) { - /* 저장된 변경 완료 플래그 복원 */ + /* 패스키 재변경 차단 플래그 복원 */ if (len != sizeof(m_passkey_changed)) { DBG_ERR("[NVS] passkey_changed size mismatch (%u)\r\n", (uint32_t)len); @@ -139,18 +147,13 @@ static int app_nvs_settings_set(const char *name, size_t len, return -ENOENT; } -SETTINGS_STATIC_HANDLER_DEFINE(app_nvs_settings, - APP_NVS_SETTINGS_NAME, - NULL, - app_nvs_settings_set, - NULL, - NULL); +/* Zephyr settings에 vesiscan 저장소만 등록 */ +SETTINGS_STATIC_HANDLER_DEFINE(app_nvs_settings, APP_NVS_SETTINGS_NAME, NULL, app_nvs_settings_set, NULL, NULL); #endif -int app_nvs_init(piezo_config_t *piezo_cfg, - app_nvs_piezo_validate_fn piezo_validate, - app_nvs_piezo_log_fn piezo_log) +int app_nvs_init(piezo_config_t *piezo_cfg, app_nvs_piezo_validate_fn piezo_validate, app_nvs_piezo_log_fn piezo_log) { + /* settings_load_subtree가 호출되기 전에 복원용 포인터와 콜백 저장 */ m_piezo_cfg = piezo_cfg; m_piezo_validate = piezo_validate; m_piezo_log = piezo_log; @@ -178,6 +181,7 @@ int app_nvs_init(piezo_config_t *piezo_cfg, int app_nvs_save_piezo(const piezo_config_t *cfg) { #if IS_ENABLED(CONFIG_SETTINGS) + /* 현재 측정 파라미터 저장 */ int err = settings_save_one(APP_NVS_FULL_KEY_PIEZO, cfg, sizeof(*cfg)); if (err) { @@ -193,6 +197,7 @@ int app_nvs_save_piezo(const piezo_config_t *cfg) int app_nvs_save_hw_no(const char *hw_no) { #if IS_ENABLED(CONFIG_SETTINGS) + /* 고정 길이 HW Version 저장 */ int err = settings_save_one(APP_NVS_FULL_KEY_HW_NO, hw_no, HW_NO_LENGTH); if (err) { @@ -208,6 +213,7 @@ int app_nvs_save_hw_no(const char *hw_no) int app_nvs_save_serial_no(const char *serial_no) { #if IS_ENABLED(CONFIG_SETTINGS) + /* 고정 길이 Serial Number 저장 */ int err = settings_save_one(APP_NVS_FULL_KEY_SERIAL_NO, serial_no, SERIAL_NO_LENGTH); if (err) { @@ -223,6 +229,7 @@ int app_nvs_save_serial_no(const char *serial_no) int app_nvs_save_passkey(const char *passkey) { #if IS_ENABLED(CONFIG_SETTINGS) + /* mpz? 명령이 허용된 뒤 고정 길이 패스키 저장 */ int err = settings_save_one(APP_NVS_FULL_KEY_PASSKEY, passkey, PASSKEY_LENGTH); if (err) { @@ -238,7 +245,7 @@ int app_nvs_save_passkey(const char *passkey) int app_nvs_save_passkey_changed(uint8_t changed) { #if IS_ENABLED(CONFIG_SETTINGS) - /* 패스키 변경 성공 후 재부팅해도 재변경 차단 */ + /* 재부팅 후에도 mpz?(패스키 변경) 재실행을 막기 위한 플래그 저장(NVS) */ int err = settings_save_one(APP_NVS_FULL_KEY_PASSKEY_CHANGED, &changed, sizeof(changed)); if (err) { diff --git a/src/ble/ble_service.c b/src/ble/ble_service.c index f4e41b3..930d7be 100644 --- a/src/ble/ble_service.c +++ b/src/ble/ble_service.c @@ -15,8 +15,12 @@ #include #include #include +#if IS_ENABLED(CONFIG_SETTINGS) +#include +#endif #include #include +#include #include "ble_service.h" #include "main.h" @@ -30,6 +34,12 @@ LOG_MODULE_REGISTER(ble_svc, LOG_LEVEL_INF); *============================================================================*/ static struct bt_conn *current_conn; static ble_data_rx_cb_t rx_callback; +static bool dfu_advertising_mode; + +#if IS_ENABLED(CONFIG_BT_SMP) +#define BLE_REQUIRED_SECURITY_LEVEL BT_SECURITY_L4 +static uint32_t pairing_passkey = BT_PASSKEY_RAND; +#endif /* * NUS 전송은 "보냈다" 호출만 한다고 바로 끝나는 게 아니다. @@ -118,6 +128,142 @@ static int ble_configure_fixed_identity(void) return 0; } +#if IS_ENABLED(CONFIG_BT_SMP) +static bool ble_conn_is_secure(const struct bt_conn *conn) +{ + return (conn != NULL) && (bt_conn_get_security(conn) >= BLE_REQUIRED_SECURITY_LEVEL); +} + +static int ble_passkey_to_uint(const char *passkey, unsigned int *value) +{ + unsigned int parsed = 0U; + + if ((passkey == NULL) || (value == NULL)) + { + return -EINVAL; + } + + for (size_t i = 0; i < PASSKEY_LENGTH; i++) + { + if ((passkey[i] < '0') || (passkey[i] > '9')) + { + return -EINVAL; + } + + parsed = (parsed * 10U) + (unsigned int)(passkey[i] - '0'); + } + + *value = parsed; + return 0; +} + +static int ble_security_configure(void) +{ + unsigned int passkey; + int err = ble_passkey_to_uint(m_static_passkey, &passkey); + + if (err) + { + DBG_ERR("[BLE] Invalid stored passkey, using default\r\n"); + memset(m_static_passkey, 0, sizeof(m_static_passkey)); + memcpy(m_static_passkey, DEFAULT_PASSKEY, PASSKEY_LENGTH); + + err = ble_passkey_to_uint(m_static_passkey, &passkey); + if (err) + { + return err; + } + } + + pairing_passkey = passkey; + DBG_CORE("[BLE] Fixed passkey configured\r\n"); + return 0; +} + +static void auth_pairing_confirm(struct bt_conn *conn) +{ + int err = bt_conn_auth_pairing_confirm(conn); + + if (err) + { + DBG_ERR("[BLE] Pairing confirm failed err=%d\r\n", err); + return; + } + + DBG_CORE("[BLE] Pairing confirmed\r\n"); +} + +static void auth_cancel(struct bt_conn *conn) +{ + ARG_UNUSED(conn); + DBG_ERR("[BLE] Pairing cancelled\r\n"); +} + +#if IS_ENABLED(CONFIG_BT_APP_PASSKEY) +static uint32_t auth_app_passkey(struct bt_conn *conn) +{ + unsigned int passkey; + + ARG_UNUSED(conn); + + if (ble_passkey_to_uint(m_static_passkey, &passkey) == 0) + { + return passkey; + } + + DBG_ERR("[BLE] Invalid runtime passkey, using configured fallback\r\n"); + return pairing_passkey; +} +#endif + +static struct bt_conn_auth_cb auth_cb = +{ + .pairing_confirm = auth_pairing_confirm, + .cancel = auth_cancel, +#if IS_ENABLED(CONFIG_BT_APP_PASSKEY) + .app_passkey = auth_app_passkey, +#endif +}; + +static void auth_pairing_complete(struct bt_conn *conn, bool bonded) +{ + ARG_UNUSED(conn); + DBG_CORE("[BLE] Pairing complete bonded=%u\r\n", bonded ? 1U : 0U); +} + +static void auth_pairing_failed(struct bt_conn *conn, enum bt_security_err reason) +{ + DBG_ERR("[BLE] Pairing failed reason=%u\r\n", reason); + (void)bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL); +} + +static struct bt_conn_auth_info_cb auth_info_cb = +{ + .pairing_complete = auth_pairing_complete, + .pairing_failed = auth_pairing_failed, +}; + +static int ble_security_init(void) +{ + int err = bt_conn_auth_cb_register(&auth_cb); + + if (err) + { + DBG_ERR("[BLE] auth cb register failed err=%d\r\n", err); + return err; + } + + err = bt_conn_auth_info_cb_register(&auth_info_cb); + if (err) + { + DBG_ERR("[BLE] auth info cb register failed err=%d\r\n", err); + return err; + } + + return ble_security_configure(); +} +#endif + static void adv_restart_handler(struct k_work *work) { ARG_UNUSED(work); @@ -136,13 +282,14 @@ static const struct bt_data ad[] = static const struct bt_data sd[] = { - /* - * Legacy NUS + DFU(SMP) UUIDs together exceed the 31-byte scan response - * limit for legacy advertising. Keep the device name in the advertising - * packet and expose only SMP here so DFU remains discoverable. - - BT_DATA_BYTES(BT_DATA_UUID128_SOME, SMP_BT_SVC_UUID_VAL), // BLE DFU 동작을 위한 SMP UUDI 광고 */ - BT_DATA_BYTES(BT_DATA_UUID128_SOME, BT_UUID_NUS_VAL), // UUID에 NUS 안 넣으면 Web bluetooth 앱에서 안 보임 + /* 평상시 Web Bluetooth 앱이 찾을 수 있도록 NUS UUID 광고 */ + BT_DATA_BYTES(BT_DATA_UUID128_SOME, BT_UUID_NUS_VAL), +}; + +static const struct bt_data sd_dfu[] = +{ + /* DFU 진입 후 mcumgr 앱이 찾을 수 있도록 SMP UUID 광고 */ + BT_DATA_BYTES(BT_DATA_UUID128_SOME, SMP_BT_SVC_UUID_VAL), }; /*============================================================================== @@ -175,6 +322,17 @@ static void connected(struct bt_conn *conn, uint8_t err) }; bt_conn_le_param_update(conn, &conn_param); +#if IS_ENABLED(CONFIG_BT_SMP) + err = bt_conn_set_security(conn, BLE_REQUIRED_SECURITY_LEVEL); + if (err) + { + DBG_ERR("[BLE] Security request failed err=%d\r\n", err); + (void)bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL); + return; + } + DBG_CORE("[BLE] Security requested level=%u\r\n", BLE_REQUIRED_SECURITY_LEVEL); +#endif + led_set_state(LED_STATE_OFF); DBG_CORE("[BLE] Connected\r\n"); } @@ -201,11 +359,30 @@ static void le_param_updated(struct bt_conn *conn, uint16_t interval, interval, latency, timeout); } +#if IS_ENABLED(CONFIG_BT_SMP) +static void security_changed(struct bt_conn *conn, bt_security_t level, + enum bt_security_err err) +{ + ARG_UNUSED(conn); + + if (err) + { + DBG_ERR("[BLE] Security failed level=%u err=%u\r\n", level, err); + return; + } + + DBG_CORE("[BLE] Security ready level=%u\r\n", level); +} +#endif + BT_CONN_CB_DEFINE(conn_callbacks) = { .connected = connected, .disconnected = disconnected, .le_param_updated = le_param_updated, +#if IS_ENABLED(CONFIG_BT_SMP) + .security_changed = security_changed, +#endif }; /*============================================================================== @@ -213,7 +390,15 @@ BT_CONN_CB_DEFINE(conn_callbacks) = *============================================================================*/ static void nus_received(struct bt_conn *conn, const uint8_t *data, uint16_t len) { +#if IS_ENABLED(CONFIG_BT_SMP) + if (!ble_conn_is_secure(conn)) + { + DBG_ERR("[NUS RX] blocked until BLE security is ready\r\n"); + return; + } +#else ARG_UNUSED(conn); +#endif DBG_CORE("[NUS RX] len=%u\r\n", len); if (rx_callback) @@ -264,8 +449,26 @@ int ble_service_init(ble_data_rx_cb_t rx_cb) } DBG_CORE("[BLE] Stack enabled\r\n"); +#if IS_ENABLED(CONFIG_BT_SETTINGS) + err = settings_load_subtree("bt"); + if (err) + { + DBG_ERR("[BLE] BT settings load failed err=%d\r\n", err); + return err; + } + DBG_CORE("[BLE] BT settings loaded\r\n"); +#endif + ble_log_local_identity(); +#if IS_ENABLED(CONFIG_BT_SMP) + err = ble_security_init(); + if (err) + { + return err; + } +#endif + /* Initialize NUS */ err = bt_nus_init(&nus_cb); if (err) @@ -281,6 +484,8 @@ int ble_service_init(ble_data_rx_cb_t rx_cb) int ble_advertising_start(void) { int err; + const struct bt_data *scan_data = dfu_advertising_mode ? sd_dfu : sd; + size_t scan_data_count = dfu_advertising_mode ? ARRAY_SIZE(sd_dfu) : ARRAY_SIZE(sd); struct bt_le_adv_param adv_param = BT_LE_ADV_PARAM_INIT( BT_LE_ADV_OPT_CONN, @@ -289,14 +494,14 @@ int ble_advertising_start(void) NULL /* undirected */ ); - err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); + err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), scan_data, scan_data_count); if (err) { DBG_ERR("[BLE] Adv start failed (err %d)\r\n", err); return err; } - DBG_PRINTF("[BLE] Advertising started\r\n"); + DBG_PRINTF("[BLE] Advertising started (%s)\r\n", dfu_advertising_mode ? "SMP" : "NUS"); return 0; } @@ -313,6 +518,38 @@ int ble_advertising_stop(void) return 0; } +int ble_dfu_advertising_enable(void) +{ + if (dfu_advertising_mode) + { + DBG_ERR("[BLE] DFU advertising already enabled\r\n"); + return -EALREADY; + } + + /* mdf? 응답 전송 후 현재 연결을 끊고 SMP UUID 광고로 재시작 */ + dfu_advertising_mode = true; + DBG_PRINTF("[BLE] DFU advertising mode enabled\r\n"); + + if (current_conn) + { + int err = bt_conn_disconnect(current_conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); + if (err) + { + DBG_ERR("[BLE] DFU disconnect failed err=%d\r\n", err); + return err; + } + return 0; + } + + (void)ble_advertising_stop(); + return ble_advertising_start(); +} + +bool ble_dfu_advertising_is_enabled(void) +{ + return dfu_advertising_mode; +} + int ble_data_send(const uint8_t *data, uint16_t len) { int err; @@ -323,6 +560,14 @@ int ble_data_send(const uint8_t *data, uint16_t len) return -ENOTCONN; } +#if IS_ENABLED(CONFIG_BT_SMP) + if (!ble_conn_is_secure(current_conn)) + { + DBG_ERR("[NUS TX] blocked until BLE security is ready\r\n"); + return -EACCES; + } +#endif + /* * NUS는 한 번에 한 패킷씩 질서 있게 보내는 편이 안전하다. * 특히 mbb?는 rbb + reb x6 + raa처럼 여러 응답을 연속 전송하므로 diff --git a/src/ble/ble_service.h b/src/ble/ble_service.h index 4712d07..5896e5f 100644 --- a/src/ble/ble_service.h +++ b/src/ble/ble_service.h @@ -15,7 +15,7 @@ /*============================================================================== * BLE configuration *============================================================================*/ -#define BLE_DEV_MODE 1 /* 1: Dev (no security), 0: Production */ +#define BLE_DEV_MODE 0 /* 1: Dev (no security), 0: Production */ #define APP_ADV_INTERVAL 64 /* 64 x 0.625ms = 40ms */ #define APP_ADV_DURATION 18000 /* 18000 x 10ms = 180s (3 min) */ @@ -39,6 +39,8 @@ int ble_service_init(ble_data_rx_cb_t rx_cb); int ble_advertising_start(void); int ble_advertising_stop(void); int ble_data_send(const uint8_t *data, uint16_t len); +int ble_dfu_advertising_enable(void); +bool ble_dfu_advertising_is_enabled(void); bool ble_is_connected(void); #endif /* BLE_SERVICE_H__ */