Android 设备认证 SDK
SDK Version: 1.0.0
Package:com.xtalk.mesh.sdk.auth
产品用途
能力概览
设备认证 SDK 在 BLE 或 UART 的 AT 通道上完成设备身份门槛:选择 BLE 可用的 plain/XOR AT 编码;需要 Challenge 验签时,只有 Challenge payload 可解析、签名验证通过,并且该 payload 后收到独立的 AT_OK,当前连接才进入已认证状态。随后 SDK 再独立读取结构化 DID,并尝试把 DID 应用到 xTalk 核心身份状态。
认证只证明当前连接上的设备身份。DID 获取、频率、速率、附加地址、厂商默认参数、RF 默认参数和工作模式共同决定业务 readiness,不能视为认证门槛的一部分。
职责与边界
SDK 负责
- 生成随机 Challenge、解析设备回应、验证签名,并要求 Challenge payload 后出现独立终态
AT_OK。 - BLE 下先等待连接/GATT 就绪,再探测 plain/XOR AT 路径。
- 优先用
AT+EFUSESN?读取结构化 DID,识别+EFUSESN和真机固件返回的+SN别名;只有该响应无效时才调用AT+SN?。 - 在 SDK 内校验 SN 字段、调用统一 codec 构造 DID,并通过
Result.deviceLongId返回。 - 将常见链路、命令、验签和 DID 失败转换为稳定的
Result.reason。 - 为 BLE 提供进程级并发合并入口,避免同时发起多次 Challenge。
宿主负责
- 先建立 BLE 或 UART 链路。BLE 使用 SDK 托管入口,不自行构造 BLE
RadioTransport;UART 提供可串行执行 AT 命令的RadioTransport或AtCommandTransport。 - 在后台线程调用认证方法;用
Result.ok/Result.authenticated判断身份门槛,取得 DID 后才能执行 Device Init。 - 不记录 Challenge 原始材料、签名全文或完整认证回应,不允许绕过验签失败。
- 在设备重启、OTA 完成、链路身份变化或认证失效时清除旧状态并重新认证。
- 不得仅因 DID 读取/DID 应用、默认参数、厂商初始化或 RF 初始化失败,就撤销当前连接的认证或再次发送 Challenge。
- 根据
reason提示、重连或停止流程;不得只因读到一个数字就视为认证成功。 - 直接使用
auth.deviceLongId;不得从原始+SN/+EFUSESN行拼接或计算 DID。
适用范围与前提
支持链路
- BLE:使用
XTalkBleClient当前已连接且 GATT 就绪的链路;认证入口在 License 校验通过后由 SDK 内部取得受保护 transport。 - UART:使用客户实现的
DeviceAuthenticator.AtCommandTransport,或由 UART SDK/RadioTransport 适配。 - Android
minSdk 26,SDK 以compileSdk 36和 JVM 17 构建。
调用前提
- BLE 调用前应已收到
CONNECTED。认证方法仍会等待连接和 GATT,但它不能代替扫描、连接或权限处理。 - UART 必须已打开,并能返回按 CR/LF 拆分的完整 AT 行。
- 同一物理通道不得同时执行 Auth、Init、OTA 或其他 AT 会话。
Result.authenticated与Result.ok完全相等;两者都只报告设备身份门槛。DID/readiness 必须另行检查deviceLongId和reason。
产物与依赖
Release AAR
| 项目 | 当前交付事实 |
|---|---|
| 主产物 | device-auth-sdk-release.aar |
| 直接依赖 | ble-sdk-release.aar、radio-transport-sdk-release.aar、xTalk SDK 1.0.0 核心依赖包;BLE 的实际依赖还包括 androidx.core:core-ktx:1.18.0 |
| 本页完整示例额外依赖 | device-init-sdk-release.aar、org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 |
| 独立 Maven 坐标 | 本认证适配产物当前没有独立远程 Maven 坐标 |
| 聚合依赖 | 供应商授权的 Maven 端点可使用 com.xtalk:xtalk-sdk:1.0.0;仓库地址以交付通知为准 |
纯 AAR 交付时,应按交付清单同时引入上述 Release AAR 和完整核心依赖。flat AAR 没有 POM 传递依赖信息,必须显式加入 BLE 所需 AndroidX 及本页完整示例使用的 Init/Coroutines。
Gradle 示例
dependencies {
implementation("com.xtalk:xtalk-sdk:1.0.0")
implementation(files("libs/radio-transport-sdk-release.aar"))
implementation(files("libs/ble-sdk-release.aar"))
implementation(files("libs/device-auth-sdk-release.aar"))
implementation(files("libs/device-init-sdk-release.aar"))
implementation("androidx.core:core-ktx:1.18.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
权限与项目配置
Android 权限
认证 AAR 自身不声明新的 Android 运行时权限。BLE 认证继承 BLE 连接 SDK 的扫描/连接权限;UART 认证继承宿主设备节点访问要求。认证算法本身不需要麦克风、通知、存储或网络权限。
安全配置
- 使用 JDK 17 和 JVM target 17。
- 禁止把 Challenge 请求、签名回应或可还原认证材料写入客户分析平台。
AtCommandTransport.label只填写稳定、非敏感的通道标签,例如uart,不要包含密钥、DID 或用户信息。- 如需记录结果,只记录成功/失败、
reason和脱敏设备标识。
公开类型
DeviceAuthenticator
DeviceAuthenticator 是 Kotlin object,不需要构造。其进程级 BLE exclusive 状态会被所有调用者共享。
| 类型 | 公开契约 |
|---|---|
AtCommandTransport | 客户 AT 通道接口:label: String;runAtCommand(command: String, timeoutMs: Long, log: Boolean = true): List<String>。 |
Result(ok: Boolean, deviceLongId: Long?, reason: String) | 认证结果及可选 DID。authenticated 是 ok 的只读别名。reason == "did_read_failed" 时为 ok == true、authenticated == true、deviceLongId == null:身份认证已通过,但业务尚未 ready。 |
AtCommandTransport 实现契约
runAtCommand 必须串行发送一条命令,并返回按 CR/LF 分隔后的完整行。关闭、设备错误、发送失败和超时应抛异常;Auth 会把认证阶段的常见异常转换为失败结果。timeoutMs 是该次命令等待期限,log=false 时实现方应关闭普通 AT 内容日志。
初始化与生命周期
无独立初始化
认证模块没有 init、close 或 deinit。它不拥有 BLE/UART 链路;链路创建与销毁由对应 transport 的生命周期所有者完成。
BLE 认证生命周期
连接 → GATT ready → authenticateBleAndReadDid... → 要求 Result.ok/Result.authenticated 为真 → 要求 DID 非空 → Device Init。DID 或 Init 失败只会让业务保持不可发送,不会撤销当前连接认证。只有断链、设备重启、OTA、身份变化、显式作废或认证门槛失败后才重新认证。
UART 认证生命周期
打开 UART → 捕获当前连接 token → 调用 token-bound authenticateUart → 只把认证提交给同一连接 → 使用同一 token 读取 DID → UART 初始化。DID/默认参数/厂商/RF 初始化失败时,同一连接仍保持已认证,不得再次发送 Challenge。关闭 UART 由宿主负责。
完整 API 参考
AT 通道适配
| 接口 | 参数、返回与错误 |
|---|---|
AtCommandTransport.label: String | 人类可读通道标签,只用于日志。不得包含秘密。 |
AtCommandTransport.runAtCommand(command, timeoutMs, log = true): List<String> | 返回完整回应行;实现方负责超时、错误异常和串行化。 |
SDK 的 RadioTransport 重载会在内部创建 AT 适配器,客户不需要、也不能构造公开的 Auth RadioAtCommandTransport。 |
认证状态
| 接口 | 参数、返回与副作用 |
|---|---|
markAuthenticationRequired(reason: String, resetAtMode: Boolean = true) | 清 BLE wireless-ready、取消核心初始化序列和进程级 exclusive 结果;默认重置当前 BLE AT 编码。无返回值。 |
applyDeviceLongId(deviceLongId: Long, source: String): Boolean | 将 DID 格式化后应用到核心身份管理器;应用成功为 true。source 只用于日志且不得敏感。 |
认证方法
| 接口 | 线程/前提 | 默认值和当前行为 | 返回与失败 |
|---|---|---|---|
authenticateBleAndReadDid(timeoutMs: Long = 30000): Result | 后台线程;XTalkBleClient 已连接且 GATT ready | 推荐客户入口。License 校验通过后由 SDK 内部创建受保护 BLE transport。当前 leader 流程采用固定阶段期限:连接 8 s、GATT 8 s、每种 AT 模式探测 1.5 s、Challenge 6 s、DID 4 s;timeoutMs 当前保留在签名中,不改变这些阶段期限 | 选择 AT 模式,必要时执行 Challenge,再尝试读取 DID;ok 与 authenticated 相等,进入 Init 前需另行要求 deviceLongId 非空 |
authenticateBleAndReadDidExclusive(timeoutMs: Long = 30000, forceReauth: Boolean = false): Boolean | 后台线程;同一进程只允许一个 leader | 推荐并发合并入口。并发 caller 加入当前任务,最多等待 timeoutMs + 8000;forceReauth 先清状态。顺序调用仍会重新认证,不使用成功缓存跳过流程 | 认证 Boolean,等于 leader 的 Result.ok;需要业务 ready 的 caller 仍须另行取得 DID。joiner 超时或 leader 失败为 false,不启动第二个 Challenge |
authenticateBleAndReadDid(radioTransport, timeoutMs: Long = 30000): Result | 后台线程;已由其他公开产品入口提供 BLE RadioTransport | 高级组合重载;基础 BLE SDK 1.0.0 不向客户提供该 transport 的构造器或工厂 | 返回语义与托管入口相同 |
authenticateBleAndReadDidExclusive(radioTransport, timeoutMs: Long = 30000, forceReauth: Boolean = false): Boolean | 后台线程;已由其他公开产品入口提供 BLE RadioTransport | 高级组合重载;不用于基础 BLE 客户接入 | 返回语义与托管 exclusive 入口相同 |
authenticateUart(radioTransport, expectedConnectionToken, challengeTimeoutMs: Long = 10000): Result | 后台线程;使用支持连接 token 的 UART RadioTransport 当前 token | 完整 Challenge 事务绑定 expectedConnectionToken;token 过期或 transport 缺少该能力时,不会降级执行普通 AT 写入 | Challenge 签名及其独立终态 AT_OK 被接受后立即返回:ok == authenticated == true、deviceLongId == null、reason == "ok" |
authenticateUartAndReadDid(transport, challengeTimeoutMs: Long = 10000, didTimeoutMs: Long = 6000): Result | 后台线程;UART 已打开且由 caller 独占 | 兼容自定义 AT 通道;两个期限分别作用于 Challenge 和每次 DID 查询 | Challenge 失败为 ok == authenticated == false;Challenge 已通过但 DID 失败时为 ok == authenticated == true、deviceLongId == null、reason == "did_read_failed" |
authenticateUartAndReadDid(radioTransport, challengeTimeoutMs: Long = 10000, didTimeoutMs: Long = 6000): Result | 后台线程;caller 持有稳定 UART 连接 | 不带 expected token 的兼容重载 | 返回语义同自定义 transport 重载;存在重连替换物理连接的可能时,优先使用 token-bound authenticateUart 再执行 token-bound DID 读取 |
readDeviceLongId(transport, timeoutMs: Long, log: Boolean): Long? | 后台线程;AT 通道可用 | 依次执行 AT+EFUSESN?、AT+SN?;timeoutMs 分别用于每条命令 | 返回第一个结构合法的 DID;异常、超时或无合法结构返回 null |
readDeviceLongId(radioTransport, expectedConnectionToken, timeoutMs: Long, log: Boolean): Long? | 后台线程;使用 UART 认证时的同一连接 token | 两个 DID 查询都绑定 expectedConnectionToken,不使用无绑定 AT 降级路径 | 返回该连接上的合法 DID,否则为 null |
SN 响应字段与 DID 映射
AT+EFUSESN? 可以返回 +EFUSESN:、+EFUSESN 、+SN: 或真机固件使用的
+SN 。结构化响应必须有 7 段或 8 段;前 6 段是 ASCII 十进制,第 7 段
是 ASCII 十六进制序列号,第 8 段是可选的 ASCII 十六进制 EFUSE/追溯字段。
| 段号 | 真机值 | 格式 | SDK 含义 |
|---|---|---|---|
| 1 | 60 | ASCII 十进制 UInt32 | 制造元数据 1;校验但不参与 DID |
| 2 | 002 | ASCII 十进制 UInt32 | 制造元数据 2;校验但不参与 DID |
| 3 | 24 | ASCII 十进制 UInt32 | year,取低 5 bit 写入 DID |
| 4 | 24 | ASCII 十进制 UInt32 | week,取低 7 bit 写入 DID |
| 5 | 11 | ASCII 十进制 UInt32 | 制造元数据 5;校验但不参与 DID |
| 6 | 4 | ASCII 十进制 UInt32 | 制造元数据 6;校验但不参与 DID |
| 7 | 1EE70 | ASCII 十六进制 UInt32 | serial,取低 20 bit 写入 DID |
| 8 | 00025018 | ASCII 十六进制 UInt32 | 可选 EFUSE/追溯字段;校验但不参与 DID |
真机响应:
AT+EFUSESN?
+SN 60:002:24:24:11:4:1EE70:00025018
AT_OK
Android 和 iOS 使用相同字段映射:
(24 & 0x1F) << 27 | (24 & 0x7F) << 20 | (0x1EE70 & 0xFFFFF)
= 0xC181EE70
因此 Android 返回 auth.deviceLongId == 0xC181EE70L,且不会继续调用
AT+SN?。第 8 段 00025018 不进入 DID;任一字段格式不合法时,整条响应
无效。业务端只使用 auth.deviceLongId,不得从原始 AT 回应手算 DID。
Challenge payload 可解析、签名验证通过,并收到该 payload 后的独立 AT_OK 时,当前连接立即进入已认证状态;Result.authenticated 与 Result.ok 报告同一个身份状态。DID 是否可用由 deviceLongId 独立报告;UART 组合方法还会用 reason == "did_read_failed" 表示 DID 阶段失败。
token-bound UART 认证前先捕获 radioTransport.currentConnectionToken,并原样传入。该事务不会向替换后的连接写入,也不会消费替换连接的回应。完整 Challenge 终态已被消费后,之后发生的 token 变化不会追溯改写返回结果;宿主把认证写入自身状态前,仍必须确认捕获的 token 当前有效。
DID 读取/DID 应用、默认参数、厂商初始化和 RF 初始化都属于 readiness 阶段。它们失败时不得清除认证,也不得在同一连接上重复 Challenge。只有断链、设备重启/OTA、连接身份替换、显式调用 markAuthenticationRequired 或认证门槛失败,才需要新的 Challenge。applyDeviceLongId 的独立 Boolean 结果不包含在 Result 中;如需确认核心身份已发布,应在后续连接层查询当前 DID。
Result.reason
| reason | 含义 | 建议处理 |
|---|---|---|
ok | 认证门槛成功;challenge-only UART 尚未读取 DID,组合方法也必须检查 deviceLongId | 只有其他 readiness 条件都满足后才能进入业务 |
not_connected | 8 秒内未进入 BLE CONNECTED | 检查状态并重连 |
ble_not_ready | 8 秒内 GATT/AT 路径未就绪 | 断开重连,不要循环空转 Auth |
at_path_unavailable | plain/XOR AT+FREQ? 均未形成可接受回应 | 检查固件兼容和链路 |
challenge_command_failed | Challenge 命令异常、设备错误或超时 | 保持未认证;确认链路后最多重试一次 |
challenge_verify_failed | Challenge payload/签名/终态 AT_OK 校验失败;authenticated == false | 不得绕过;断开并联系技术支持 |
did_read_failed | Challenge 认证已成功,但两个 DID 查询都没有合法结构化 ID;ok == authenticated == true、deviceLongId == null | 保持当前连接已认证、保持业务不可用,只恢复 DID/readiness 阶段,不得再次 Challenge |
分功能示例
BLE 认证
val result = withContext(Dispatchers.IO) {
DeviceAuthenticator.authenticateBleAndReadDid(
timeoutMs = 30_000L,
)
}
if (!result.ok) {
handleAuthenticationFailure(result.reason)
} else if (result.deviceLongId == null) {
handleDidReadinessFailure(result.reason)
} else {
onAuthenticated(requireNotNull(result.deviceLongId))
}
合并并发 BLE 认证
val authenticated = withContext(Dispatchers.IO) {
DeviceAuthenticator.authenticateBleAndReadDidExclusive(
timeoutMs = 30_000L,
forceReauth = false,
)
}
绑定连接的 UART 认证与 DID 读取
val connectionToken = requireNotNull(uartRadioTransport.currentConnectionToken)
val auth = DeviceAuthenticator.authenticateUart(
radioTransport = uartRadioTransport,
expectedConnectionToken = connectionToken,
challengeTimeoutMs = 10_000L,
)
check(auth.ok)
check(uartRadioTransport.isConnectionTokenCurrent(connectionToken))
val deviceLongId = DeviceAuthenticator.readDeviceLongId(
radioTransport = uartRadioTransport,
expectedConnectionToken = connectionToken,
timeoutMs = 6_000L,
log = false,
)
if (deviceLongId == null) {
keepAuthenticatedButNotReady()
} else {
continueInitialization(deviceLongId, connectionToken)
}
主动作废认证
DeviceAuthenticator.markAuthenticationRequired(
reason = "device_reboot_or_ota",
resetAtMode = true,
)
完整接入示例
BLE 连接和认证
suspend fun authenticateBleDevice(device: XTalkDevice): Long? =
withContext(Dispatchers.IO) {
if (XTalkBleClient.connect(device, timeoutMs = 8_000) != ConnectResult.CONNECT_OK) return@withContext null
if (!XTalkBleClient.awaitConnected(device.macAddress, 8_000L)) return@withContext null
if (!XTalkBleClient.awaitBleReady(8_000L)) return@withContext null
val auth = DeviceAuthenticator.authenticateBleAndReadDid()
if (!auth.ok) {
XTalkBleClient.disconnect(sendRemoteCommand = false)
return@withContext null
}
auth.deviceLongId
}
基础 BLE SDK 1.0.0 已提供可直接复制的托管 Auth 入口,但尚未提供客户可取得
通用 BLE RadioTransport 的公开工厂。需要 Device Init 或通用 RF transport
时,必须使用交付产品明确提供的公开工厂/实例;不要自行调用非公开构造器。
状态/并发/资源与恢复
线程
所有认证、DID 读取和客户 runAtCommand 都是阻塞式 API,必须在 Dispatchers.IO 或专用工作线程调用。不要用“放入协程”误认为底层可取消;协程取消不保证中断正在执行的 AT 调用。
并发
BLE 推荐使用 authenticateBleAndReadDidExclusive 合并进程内并发请求。第一个 caller 是 leader,其他 caller 只等待同一结果。UART 没有内建全局锁,客户的 AtCommandTransport 必须与 Init、OTA 和业务 AT 串行。
资源与恢复
认证模块不持有 Activity、BLE 连接或 UART 文件描述符,因此没有独立释放接口。取消业务时停止后续步骤,等待认证调用返回,再由链路所有者断开。DID/默认参数/厂商/RF readiness 失败时,应保留同一连接的认证,只恢复失败的 readiness 阶段。只有连接或认证状态失效后才重连并重新 Challenge;验签失败不得自动降级或跳过。
常见问题
BLE 返回 not_connected
connect() 的成功枚举只表示请求已接受。先等待 CONNECTED,确认没有其他逻辑主动断开,再调用认证。
BLE 返回 at_path_unavailable
plain 和 XOR 两条 AT 路径都未通过频率回应门槛。检查 GATT ready、固件版本和是否存在并发 AT/OTA,不要手工固定一种编码绕过探测。
返回 challenge_verify_failed
这是身份门槛失败,不是普通网络抖动。立即保持设备不可用并断开,不记录或上传完整响应,联系供应商分析。
返回 did_read_failed
Challenge 已通过,所以当前连接的 ok 与 authenticated 都保持 true,但 deviceLongId 为 null。保持业务不可用,只恢复 DID/readiness,不要再次发送 Challenge。若连接已替换、设备已重启或认证被显式作废,则按新连接正常重新认证。
Exclusive 调用等待很久
caller 可能加入了已经执行中的认证;joiner 最多等待自身 timeoutMs + 8000。超时返回 false,不会取消 leader,也不会并行发第二次 Challenge。
关联 SDK 与下一步
接入顺序
- BLE 链路先完成 BLE 连接 SDK 的连接和 GATT ready。
- UART 链路先完成 UART 传输 SDK 的打开和 AT 通道适配。
- 认证成功后调用 设备初始化 SDK。
- 统一 BLE/UART 调用时参考 RadioTransport。