加密
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_cb的auth_info.crypto_algo字段读取实际采用的算法。
案例说明
做什么
Server 注册两个 Property,对比加密前后的行为差异:
- 公开属性(handle 1):
SSAP_PERMISSION_READ——不加加密权限,任何链路都能读取 - 加密属性(handle 2):
SSAP_PERMISSION_READ | SSAP_PERMISSION_ENCRYPTION_NEED——需要加密链路才能读取
测试流程:
- Client 连接 → 读公开属性(handle 1)成功 → 读加密属性(handle 2)被拒绝
- Client 发起配对 →
auth_complete_cb确认加密建立 - Client 读加密属性(handle 2)成功
- Client 再次读公开属性(handle 1)——此时链路已加密,返回的也是密文
加密前后行为对比
| 操作 | 加密前 | 加密后 |
|---|---|---|
| 读公开属性(handle 1) | 成功,明文传输 | 成功,密文传输 |
| 读加密属性(handle 2) | 失败:ERRCODE_SLE_NOT_ENCRYPTED |
成功,密文传输 |
| 写公开属性 | 成功 | 成功,密文传输 |
| 写加密属性 | 失败 | 成功,密文传输 |
| 通知推送 | 不加密,明文 | 加密,密文 |
关键配置
加密算法配置
加密算法在协议栈层协商确定,应用层通过 auth_complete_cb 的 sle_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 |