跳转至

Just Works 配对

使用技术:SLE 配对、绑定、加密、密钥管理

前置阅读:Hello Notify

学习目标

  • 理解 SLE 配对(Pairing)的完整流程及其在安全通信中的作用
  • 理解 Just Works 配对模型的原理和适用场景
  • 掌握配对 API 的回调链:发起配对 → pair_complete_cb → MTU 交换 → 服务发现
  • 理解绑定(Bonding)与配对的区别以及 NV 持久化的意义
  • 能够诊断常见的配对失败原因及其错误码

规格与功能

本案例演示完整的 Just Works 配对流程——从连接建立到加密通信建立的必经之路。

规格项 Server 端 Client 端
配对发起方 Client(连接成功后主动发起)
配对模式 Just Works(无需用户交互) Just Works
加密算法 EA1/EA2 (对称加密) EA1/EA2
密钥派生 HA1/HA2 HA1/HA2
绑定 配对成功后存储密钥到 NV 配对成功后存储密钥到 NV
完整性校验 启用 启用

程序运行流程:

  1. Server 和 Client 建立连接
  2. Client 在 connect_state_changed_cb 中检测 pair_state == SLE_PAIR_NONE
  3. Client 调用 sle_pair_remote_device() 发起配对
  4. 双方协议栈自动完成密钥交换和加密协商
  5. pair_complete_cb 被触发——配对成功
  6. Client 发起 MTU 交换 → Server 响应 → exchange_info_cb 触发
  7. Client 发起服务发现 → 获取 Property 句柄 → 可以开始数据通信

hello-connect 告诉了你怎么连接,但连接后不配对就无法交换 MTU、无法发现服务、无法传输数据。配对是数据通信的前置条件。

基本概念

配对 vs 绑定

配对和绑定是两个不同但相关的概念:

配对(Pairing) 绑定(Bonding)
含义 交换加密密钥,建立加密链路 将配对生成的密钥存储到 NV
有效期 本次连接有效,断开后密钥丢失 永久有效,重启保持
目的 保护本次数据传输 下次重连时可快速恢复加密,无需重新配对
API sle_pair_remote_device() 配对时配置 is_bond = SLE_PAIR_NEED_BOND
sequenceDiagram
    participant C as Client
    participant S as Server
    participant NV as NV Flash

    Note over C,S: 已连接,未配对
    C->>S: sle_pair_remote_device()
    Note over C,S: 密钥交换(DH 或 ECDH)
    Note over C,S: 加密链路建立
    Note over C: pair_complete_cb
    Note over S: pair_complete_cb
    C->>NV: 存储密钥 (bonding)
    S->>NV: 存储密钥 (bonding)
    Note over C,S: 加密通信就绪

    C--xS: 断开连接
    Note over C,S: 密钥仍保存在 NV 中

    C->>S: 重新连接
    Note over C,S: 从 NV 读取密钥,<br/>自动恢复加密,无需重新配对

Just Works 与其他配对方式的对比

配对方式 用户交互 安全性 适用场景
Just Works 无(自动完成) 基本——无法防御 MITM 无屏幕/无输入能力的嵌入式设备
Passkey Entry 输入 6 位数字 中——需要物理接触 带键盘或显示屏的设备
Numeric Comparison 确认 6 位数字 高——双方确认匹配 双方都有显示屏的设备
Out of Band (OOB) 无(通过其他通道交换) 高——带外通道保证 NFC/BLE 辅助配对

WS63 目前主要支持 Just Works 模式。这是嵌入式 IoT 设备最常见的配对方式——没有屏幕也没有键盘,只能信任第一次连接。

配对回调解耦链

配对的完成是后续所有数据操作的前提。下面的回调链是标准流程:

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: sle_pair_remote_device()
    Note over C,S: 协议栈自动完成密钥交换

    S-->>C: pair_complete_cb
    Note right of S: 配对完成 → 设置 MTU
    C-->>C: pair_complete_cb
    Note left of C: 配对完成 → 发起 MTU 交换

    C->>S: ssapc_exchange_info_req()
    S-->>C: exchange_info_cb
    Note left of C: MTU 交换完成 → 发起服务发现

    C->>S: ssapc_find_structure()
    S-->>C: find_structure_cb
    S-->>C: find_property_cb
    Note left of C: 找到所有属性

    S-->>C: find_structure_cmp_cb
    Note left of C: 服务发现完成 → 可以读写/收通知

每个回调是下一个操作的触发条件。不要在 pair_complete_cb 之前尝试交换 MTU——协议栈会拒绝。

涉及 API

API 谁调用 用途
sle_pair_remote_device() Client 发起配对请求
sle_remove_paired_remote_device() 任一方 删除指定设备的配对信息
sle_remove_all_pairs() 任一方 删除所有配对信息
sle_get_paired_devices_num() 任一方 查询已配对设备数量
sle_get_paired_devices() 任一方 获取已配对设备地址列表
sle_get_pair_state() 任一方 查询对端设备的配对状态
ssapc_exchange_info_req() Client 发起 MTU 交换(配对完成后调用)
ssapc_find_structure() Client 发起服务发现(MTU 交换完成后调用)
sle_connection_register_callbacks() 双方 注册连接回调(含 pair_complete_cbauth_complete_cb

案例说明

做什么

实现完整的配对-绑定-MTU交换-服务发现回调链,这是所有 SLE 数据通信功能的基础流程。

配对状态机

stateDiagram-v2
    [*] --> IDLE: 上电初始化
    IDLE --> ANNOUNCING: 启动广播/扫描
    ANNOUNCING --> CONNECTED: 连接建立
    CONNECTED --> PAIRING: sle_pair_remote_device()
    PAIRING --> PAIRED: pair_complete_cb
    PAIRING --> PAIR_FAILED: 配对失败
    PAIRED --> ENCRYPTED: 加密链路建立
    ENCRYPTED --> DATA_READY: MTU + 服务发现完成
    PAIR_FAILED --> CONNECTED: 重试配对
    DATA_READY --> DISCONNECTED: 连接断开
    DISCONNECTED --> ANNOUNCING: 恢复广播/扫描

关键配置

配对与绑定配置

// 注册回调时配置配对相关回调
static sle_connection_callbacks_t g_conn_callbacks = {
    .connect_state_changed_cb = conn_state_cb,
    .pair_complete_cb        = pair_complete_cb,
    .auth_complete_cb        = auth_complete_cb,
    .pair_remove_cb          = pair_remove_cb,
    // ... 其他回调
};

认证信息事件

配对完成后的认证信息事件中包含加密算法和绑定指示:

typedef struct {
    uint8_t link_key[SLE_LINK_KEY_LEN];  // 16 字节链路密钥
    uint8_t crypto_algo;                 // 加密算法: AC1/AC2/EA1/EA2
    uint8_t key_deriv_algo;              // 密钥派生: HA1/HA2
    uint8_t integr_chk_ind;              // 完整性校验指示
    uint8_t is_bond;                     // 是否绑定: SLE_PAIR_NO_BOND / SLE_PAIR_NEED_BOND
} sle_auth_info_evt_t;

代码详解

Client 端:连接后发起配对

static void conn_state_cb(uint16_t conn_id,
                           const sle_addr_t *addr,
                           sle_acb_state_t conn_state,
                           sle_pair_state_t pair_state,
                           sle_disc_reason_t disc_reason)
{
    if (conn_state == SLE_ACB_STATE_CONNECTED) {
        g_conn_id = conn_id;
        memcpy(&g_remote_addr, addr, sizeof(sle_addr_t));

        // 检查配对状态
        if (pair_state == SLE_PAIR_NONE) {
            printf("[client] connected, starting pairing...\n");
            errcode_t ret = sle_pair_remote_device(addr);
            if (ret != ERRCODE_SUCC) {
                printf("[client] pair request failed: %d\n", ret);
            }
        }
    }
    else if (conn_state == SLE_ACB_STATE_DISCONNECTED) {
        printf("[client] disconnected, reason=%d\n", disc_reason);

        if (disc_reason == SLE_DISC_REASON_PAIR_FAILED ||
            disc_reason == SLE_DISC_REASON_AUTH_FAILED) {
            // 配对/认证失败:清理无效的配对信息
            sle_remove_paired_remote_device(&g_remote_addr);
        }

        g_conn_id = 0;
        sle_start_seek();  // 恢复扫描
    }
}

配对完成回调

static void pair_complete_cb(uint16_t conn_id,
                              const sle_addr_t *addr,
                              errcode_t status)
{
    if (status != ERRCODE_SUCC) {
        printf("[client] pairing failed: %d\n", status);
        // 可选:重试或断开
        return;
    }

    printf("[client] paired successfully!\n");

    // 配对成功 → 发起 MTU 交换
    ssapc_exchange_info_req(g_client_handle, conn_id);
}

认证完成回调

static void auth_complete_cb(uint16_t conn_id,
                              const sle_auth_info_evt_t *info)
{
    printf("[client] auth complete:\n");
    printf("  crypto_algo=%d, key_deriv=%d\n",
           info->crypto_algo, info->key_deriv_algo);
    printf("  integrity_check=%d, bond=%d\n",
           info->integr_chk_ind, info->is_bond);

    if (info->is_bond == SLE_PAIR_NEED_BOND) {
        printf("[client] keys saved to NV for future reconnection\n");
    }
}

MTU 交换 → 服务发现回调链

// MTU 交换完成
static void exchange_info_cb(uint16_t conn_id, errcode_t status)
{
    if (status != ERRCODE_SUCC) {
        printf("[client] MTU exchange failed: %d\n", status);
        return;
    }
    printf("[client] MTU exchange done\n");

    // MTU 完成后 → 发起服务发现
    ssapc_find_structure(g_client_handle, conn_id);
}

// 服务发现完成
static void find_structure_cmp_cb(uint16_t conn_id, errcode_t status)
{
    if (status == ERRCODE_SUCC) {
        printf("[client] service discovery complete, data ready!\n");
        // 此时可以开始读写数据、接收通知
        g_data_ready = true;
    }
}

常见配对失败原因

错误现象 可能原因 排查方法
pair_complete_cb 返回非 0 加密算法不匹配、密钥交换超时 检查双方协议栈版本是否一致
sle_pair_remote_device() 返回 BUSY 前一次配对还在进行中 等待前一次完成再发起
配对成功但无法服务发现 MTU 交换未完成 检查 pair_complete_cb 中是否调用了 exchange_info_req
重连后需要重新配对 未启用绑定(is_bond=0 配对时设置绑定标志
重连后认证失败 NV 中密钥已过期或被覆盖 调用 sle_remove_paired_remote_device() 后重新配对

在产品代码中,配对失败后建议等 1~2 秒再重试,不要立即重试——连续快速重试会加剧协议栈负担,反而降低成功率。