跳转至

加密

SLE 链路加密、属性级加密、加密算法选择

前置阅读:Just Works 配对绑定与白名单

学习目标

  • 理解 SLE 加密的两个层级——链路加密(整条链路的数据在空中被加密)和属性级加密(指定 Property 需要加密链路才能访问)
  • 掌握加密算法选择——AC1/AC2(认证算法)和 EA1/EA2(加密算法)的适用场景与合规要求
  • 理解加密密钥派生层级——配对密钥到会话密钥的完整派生链
  • 掌握在 SSAP Property 上配置加密权限的方法,以及未加密访问的错误处理
  • 能够在 auth_complete_cb 中判断加密是否建立,并根据加密状态控制数据访问

基本概念

典型使用场景

不同应用对加密等级的需求不同:

安全等级 配置 场景举例 保护目标
链路加密 + 属性加密 配对 + SSAP_PERMISSION_ENCRYPTION_NEED 医疗数据(心率/血氧)、支付信息、门禁密码 防窃听 + 防未授权访问
仅链路加密 配对,无属性加密权限 普通传感器上报(温湿度) 防窃听,不限制访问
不加密 不配对 公开信息广播(设备名、电量) 无保护需求

加密的两个层级

SLE 提供两个独立但互补的加密层级:

层级 作用范围 配置方式 失败行为
链路加密 整条 SLE 连接的所有数据包 配对过程协商(自动) 链路未加密时数据明文传输
属性级加密 单个 SSAP Property 的读写操作 permissions |= SSAP_PERMISSION_ENCRYPTION_NEED Server 拒绝未加密链路的读写请求
sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: 连接建立
    Note over C,S: 链路未加密

    C->>S: 读公开属性 device_name
    S-->>C: 返回 device_name

    C->>S: 读加密属性 heart_rate
    S-->>C: 拒绝: NOT_ENCRYPTED

    C->>S: sle_pair_remote_device
    Note over C,S: 密钥交换 → 链路加密建立
    Note over C: auth_complete_cb
    Note over S: auth_complete_cb

    C->>S: 读加密属性 heart_rate
    S-->>C: 返回 heart_rate 密文

    C->>S: 读公开属性 device_name
    S-->>C: 返回 device_name 密文

链路加密建立后,所有数据传输都变成密文——包括公开属性和加密属性。属性级加密的作用不是"让数据变密文",而是"拒绝在未加密链路上传输本属性"。

加密算法选择

SLE 定义了两组加密算法,每组包含认证和加密两个成员:

算法族 认证算法 加密算法 底层密码 适用场景
国际算法(AC1/EA1) SLE_CRYTO_ALGO_AC1 (0x01) SLE_CRYTO_ALGO_EA1 (0x03) AES-128-CCM 国际市场、通用产品
国密算法(AC2/EA2) SLE_CRYTO_ALGO_AC2 (0x02) SLE_CRYTO_ALGO_EA2 (0x04) SM4-128-CCM 国内政企、信创场景、国密合规

AC vs EA 的分工

算法类型 用途 使用时机
AC1/AC2 认证加密——对数据进行签名和认证,确保数据未被篡改 配对阶段,验证密钥交换完整性
EA1/EA2 数据加密——对链路数据进行加密,确保窃听者无法解密 数据传输阶段,加密 payload

算法选择决策树

需要国密合规?
├── 是 → SLE_CRYTO_ALGO_AC2 + SLE_CRYTO_ALGO_EA2 (SM4)
└── 否 → SLE_CRYTO_ALGO_AC1 + SLE_CRYTO_ALGO_EA1 (AES)

双方采用的算法必须一致,否则配对失败。算法由协议栈在配对协商时自动确定,应用层在 auth_complete_cb 中读取 crypto_algo 确认实际采用的算法。

密钥派生层级

flowchart TD
    PK[配对密钥 Pairing Key] --> HA
    HA[HA1 / HA2 认证密钥派生] --> LTK
    LTK[LTK 长期密钥 16B] --> SK

    LTK -->|绑定后| NV[NV Flash 持久存储]
    NV -->|重连时读取| LTK

    SK[SK 会话密钥] --> LINK[链路加密 EA1/EA2]
    SK --> AUTH[认证签名 AC1/AC2]

各层密钥说明

层级 密钥名称 长度 生存周期 存储位置
配对密钥 Pairing Key 单次配对 RAM(配对后丢弃)
认证派生密钥 HA1/HA2 128 bit 单次配对 RAM
长期密钥 LTK 128 bit 直到删除绑定 NV Flash(绑定后)
会话密钥 SK 128 bit 单次连接 RAM(断开后丢弃)

SK 每次连接重新派生,即使 LTK 相同,不同连接产生的 SK 也不同。这保证了重放攻击无法奏效——用旧连接的密文无法在新连接中解密。

涉及 API

API 谁调用 用途
sle_pair_remote_device(&addr) Client 发起配对,触发密钥派生和加密建立
ssaps_add_property_sync() Server 注册属性时指定 SSAP_PERMISSION_ENCRYPTION_NEED 权限
sle_connection_register_callbacks() 双方 注册回调,含 auth_complete_cb——加密建立通知
ssapc_read_req() Client 读属性——加密属性需在 auth_complete_cb 之后

加密算法由协议栈在配对过程中自动协商,应用层无需(也不能)通过 API 指定算法。应用层通过 auth_complete_cbauth_info.crypto_algo 字段读取实际采用的算法。

案例说明

做什么

Server 注册两个 Property,对比加密前后的行为差异:

  • 公开属性(handle 1):SSAP_PERMISSION_READ ——不加加密权限,任何链路都能读取
  • 加密属性(handle 2):SSAP_PERMISSION_READ | SSAP_PERMISSION_ENCRYPTION_NEED ——需要加密链路才能读取

测试流程

  1. Client 连接 → 读公开属性(handle 1)成功 → 读加密属性(handle 2)被拒绝
  2. Client 发起配对 → auth_complete_cb 确认加密建立
  3. Client 读加密属性(handle 2)成功
  4. Client 再次读公开属性(handle 1)——此时链路已加密,返回的也是密文

加密前后行为对比

操作 加密前 加密后
读公开属性(handle 1) 成功,明文传输 成功,密文传输
读加密属性(handle 2) 失败:ERRCODE_SLE_NOT_ENCRYPTED 成功,密文传输
写公开属性 成功 成功,密文传输
写加密属性 失败 成功,密文传输
通知推送 不加密,明文 加密,密文

关键配置

加密算法配置

加密算法在协议栈层协商确定,应用层通过 auth_complete_cbsle_auth_info_evt_t 确认结果:

typedef struct {
    uint8_t link_key[SLE_LINK_KEY_LEN];  // 16 字节链路密钥
    uint8_t crypto_algo;                 // SLE_CRYTO_ALGO_AC1/AC2/EA1/EA2
    uint8_t key_deriv_algo;              // SLE_KEY_DERIV_ALGO_HA1/HA2
    uint8_t integr_chk_ind;              // 完整性校验
    uint8_t is_bond;                     // 绑定标志
} sle_auth_info_evt_t;

算法选择对照表:

场景 crypto_algo key_deriv_algo 底层密码
国际通用 SLE_CRYTO_ALGO_EA1 (0x03) SLE_KEY_DERIV_ALGO_HA1 (0x01) AES-128-CCM
国密合规 SLE_CRYTO_ALGO_EA2 (0x04) SLE_KEY_DERIV_ALGO_HA2 (0x02) SM4-128-CCM

属性加密权限配置

在注册 SSAP Property 时指定加密权限:

// 公开属性——不加密,任何链路都可读
ssaps_add_property_sync(
    service_handle,
    &(ssaps_property_t){
        .property_handle = 1,
        .permissions = SSAP_PERMISSION_READ,
        .uuid = {.uuid16 = 0x2A00},  // Device Name
    });

// 加密属性——只有已加密的链路才能读
ssaps_add_property_sync(
    service_handle,
    &(ssaps_property_t){
        .property_handle = 2,
        .permissions = SSAP_PERMISSION_READ | SSAP_PERMISSION_ENCRYPTION_NEED,
        .uuid = {.uuid16 = 0x2A37},  // Heart Rate Measurement
    });
权限组合 含义 适用属性
SSAP_PERMISSION_READ 可读,无加密要求 设备名、电量
SSAP_PERMISSION_READ \| SSAP_PERMISSION_ENCRYPTION_NEED 可读,需加密 心率、血压
SSAP_PERMISSION_WRITE \| SSAP_PERMISSION_ENCRYPTION_NEED 可写,需加密 配置参数、密钥
SSAP_PERMISSION_READ \| SSAP_PERMISSION_WRITE \| SSAP_PERMISSION_ENCRYPTION_NEED 可读写,需加密 支付信息、门禁指令

加密强度与开销对比

算法 密钥长度 加密开销 适用法规
AES-128-CCM (EA1) 128 bit ~300 us / packet 国际通用
SM4-128-CCM (EA2) 128 bit ~320 us / packet GB/T 32907(国密)

两种算法的加密开销差异极小(< 10%),选择依据主要是合规要求而非性能。

代码详解

1. 加密属性注册

Server 端注册两个 Property,一个有加密权限,一个没有:

static errcode_t sle_server_register_properties(uint8_t service_handle)
{
    ssaps_property_t prop = {0};
    errcode_t ret;

    /* Property 1: 设备名称——公开,不需要加密 */
    prop.property_handle = 1;
    prop.permissions     = SSAP_PERMISSION_READ;
    prop.type            = SSAP_PROPERTY_TYPE_VALUE;
    prop.uuid.uuid16     = 0x2A00; // Device Name
    ret = ssaps_add_property_sync(service_handle, &prop);
    if (ret != ERRCODE_SUCC) {
        printf("[server] add public property failed: %d\n", ret);
        return ret;
    }
    printf("[server] property 1 registered: public, no encryption needed\n");

    /* Property 2: 敏感数据——加密链路才能读 */
    prop.property_handle = 2;
    prop.permissions     = SSAP_PERMISSION_READ | SSAP_PERMISSION_ENCRYPTION_NEED;
    prop.type            = SSAP_PROPERTY_TYPE_VALUE;
    prop.uuid.uuid16     = 0x2A37; // Heart Rate Measurement
    ret = ssaps_add_property_sync(service_handle, &prop);
    if (ret != ERRCODE_SUCC) {
        printf("[server] add encrypted property failed: %d\n", ret);
        return ret;
    }
    printf("[server] property 2 registered: read requires encryption\n");

    return ERRCODE_SUCC;
}

2. 加密建立检测

auth_complete_cb 是判断加密是否建立的核心回调:

static void auth_complete_cb(uint16_t conn_id,
                              const sle_addr_t *addr,
                              errcode_t status,
                              const sle_auth_info_evt_t *info)
{
    if (status != ERRCODE_SUCC) {
        printf("[app] auth failed: %d, encryption NOT established\n", status);
        g_encryption_ready = false;
        return;
    }

    // 确认加密算法
    const char *algo_name = "unknown";
    switch (info->crypto_algo) {
        case SLE_CRYTO_ALGO_EA1: algo_name = "AES-CCM (EA1)"; break;
        case SLE_CRYTO_ALGO_EA2: algo_name = "SM4-CCM (EA2)"; break;
    }

    printf("[app] encryption established: algo=%s, key_deriv=%d, bond=%d\n",
           algo_name, info->key_deriv_algo, info->is_bond);

    g_encryption_ready = true;

    // 链路加密已建立,可以安全访问加密属性了
    if (g_pending_read_handle != 0) {
        // 之前有因未加密而失败的操作,现在重试
        ssapc_read_req(g_client_handle, conn_id, g_pending_read_handle);
        g_pending_read_handle = 0;
    }
}

auth_complete_cb 成功后,g_encryption_ready = true 是一个全局标志——后续所有数据操作都可以依赖它来判断链路是否安全。

3. 未加密时读加密属性的错误处理

Client 端在加密建立前读加密属性会被拒绝,需实现重试逻辑:

static void sle_client_read_encrypted_property(uint16_t conn_id, uint16_t property_handle)
{
    if (!g_encryption_ready) {
        // 加密未建立——暂存请求,等加密建立后重试
        printf("[client] encryption not ready, deferring read of handle %d\n",
               property_handle);
        g_pending_read_handle = property_handle;
        return;
    }

    errcode_t ret = ssapc_read_req(g_client_handle, conn_id, property_handle);
    if (ret != ERRCODE_SUCC) {
        printf("[client] read encrypted property failed: %d\n", ret);
    }
}

// 读操作完成回调
static void read_complete_cb(uint8_t client_id, uint16_t conn_id,
                              uint16_t property_handle, errcode_t status,
                              uint8_t *data, uint16_t data_len)
{
    if (status == ERRCODE_SLE_NOT_ENCRYPTED) {
        // 服务端返回"未加密"错误——说明加密还没建立或已失效
        printf("[client] server requires encryption for handle %d, deferring...\n",
               property_handle);
        g_pending_read_handle = property_handle;
        return;
    }

    if (status != ERRCODE_SUCC) {
        printf("[client] read failed: handle=%d, status=%d\n",
               property_handle, status);
        return;
    }

    printf("[client] read property handle %d: %d bytes\n",
           property_handle, data_len);
    // 处理解密后的明文数据
}

4. 绑定后重连的加密恢复

已绑定设备重连时,加密自动恢复——整个流程无需应用层干预:

static void connect_state_changed_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;

        if (pair_state == SLE_PAIR_NONE) {
            // 首次连接——发起配对
            printf("[client] starting pairing...\n");
            sle_pair_remote_device(addr);
        } else {
            // 已绑定设备重连——协议栈已从 NV 恢复 LTK
            // 派生 SK → 链路加密自动建立
            // auth_complete_cb 将自动触发,g_encryption_ready 将变为 true
            printf("[client] bonded device reconnected, "
                   "encryption will auto-recover\n");
        }
    } else if (conn_state == SLE_ACB_STATE_DISCONNECTED) {
        g_encryption_ready = false;  // 断开后加密标志复位
        g_conn_id = 0;
    }
}

重连时的加密恢复时间线

步骤 耗时 说明
连接建立 ~30 ms 物理层连接
LTK 从 NV 读取 ~2 ms 一次 Flash 读取
SK 派生 ~1 ms 纯运算
加密恢复 ~5 ms 协议栈协商
auth_complete_cb 触发 应用层通知

总计约 40ms,远快于重新配对(约 500ms~1s)。

5. 加密算法确认与合规审计

在产品固件中,建议在 auth_complete_cb 中记录加密算法的选择,方便合规审计:

static void auth_complete_cb_with_audit(uint16_t conn_id,
                                         const sle_addr_t *addr,
                                         errcode_t status,
                                         const sle_auth_info_evt_t *info)
{
    if (status != ERRCODE_SUCC) {
        return;
    }

    // 记录加密参数用于合规审计
    // 格式: [timestamp] [conn_id] algo=X key_deriv=Y bond=Z integrity=W
    printf("[AUDIT] %u conn=%d algo=%d deriv=%d bond=%d integ=%d\n",
           (unsigned)osal_sys_time_get_ms(), conn_id,
           info->crypto_algo, info->key_deriv_algo,
           info->is_bond, info->integr_chk_ind);

    // 国密合规检查
    if (REQUIRE_SM4_COMPLIANCE &&
        info->crypto_algo != SLE_CRYTO_ALGO_EA2) {
        printf("[WARNING] SM4 required but negotiated %d\n",
               info->crypto_algo);
        // 可选:断开连接,要求使用国密
    }

    g_encryption_ready = true;
}

常见问题排查

现象 可能原因 排查方法
auth_complete_cb 不触发 配对未发起或失败 检查 pair_complete_cb 是否先触发
加密属性读返回 NOT_ENCRYPTED auth_complete_cb 还未触发或属性权限配置错误 检查 auth_complete_cb 触发时序;确认 permissions 中设置了 SSAP_PERMISSION_ENCRYPTION_NEED
双方算法不匹配 协议栈配置不同 检查两边 Kconfig 中的加密算法配置
绑定重连后加密未恢复 NV 中密钥被覆盖或 NV 分区异常 检查 sle_get_paired_devices_num()
加密后写入属性失败 属性未设置 WRITE 权限 确认 permissions 包含 SSAP_PERMISSION_WRITE