本文档剖析 otxproxy 插件中实现的两项诊断安全核心算法:SeedToKey(UDS 安全访问密钥派生)与 CRC16-IBM(CA 证书数据的完整性校验)。两者均通过 algorithm.h 以 OTX_API 声明对外暴露,供 OTX 运行时和 ECU 刷写管线调用。
整体架构概览
SeedToKey 和 CRC16 虽然同为底层安全算法,但在系统中的角色截然不同。SeedToKey 是 ECU 刷写安全访问(Security Access)协议层的核心——它根据 ECU 返回的 Seed 和预置的 Mask 计算出解锁密钥,是刷写流程的前置必要条件。CRC16-IBM 则独立服务于证书数据传输场景,负责计算 CA 证书数据的 16 位校验值。
graph TD
subgraph "OTX API 导出层 (algorithm.h)"
S2K_API["seedToKey()"]
CRC_API["crc16_ibm()"]
end
subgraph "ECU 刷写管线"
BFD["BinFileDownloader"]
VFD["VbfFileDownloader"]
MCD["MCDCmdTrans"]
end
subgraph "核心算法实现 (algorithm.cpp)"
S2K_IMPL["seedToKey → calcKey()"]
CRC_IMPL["crc16_ibm()"]
end
subgraph "上层调用者"
CA_DATA["sendCAData() - CA证书打包"]
SEC_ACC["SecurityAccess() - UDS 0x27"]
SEC_ECOS["SecurityAccess_ECOS() - ECOS变体"]
end
S2K_API --> S2K_IMPL
CRC_API --> CRC_IMPL
BFD --> MCD
VFD --> MCD
MCD --> SEC_ACC
MCD --> SEC_ECOS
SEC_ACC --> S2K_IMPL
SEC_ECOS --> S2K_IMPL
CA_DATA --> CRC_IMPL图中的关键路径:SecurityAccess() 和 SecurityAccess_ECOS() 通过 UDS 0x27 服务获取 Seed 后,调用 SeedToKey() → calcKey() 计算 3 字节密钥;sendCAData() 则在证书数据打包时调用 crc16_ibm() 附加 CRC 校验。
Sources: algorithm.h, algorithm.cpp
SeedToKey:UDS 安全访问的密钥派生
协议背景
UDS(ISO 14229-1)定义了 SecurityAccess (0x27) 服务,用于在刷写或敏感操作前对 ECU 进行身份验证。标准流程分为两步:
- 请求种子 (Request Seed):诊断仪发送
0x27 0x01(Level 1 示例),ECU 返回0x67 0x01 [Seed] - 发送密钥 (Send Key):诊断仪计算密钥后发送
0x27 0x02 [Key],ECU 验证通过后解锁
otxproxy 实现了多种安全等级(Level 1–11),覆盖普通刷写(pinCode)和换件刷写(FFFFFFFF)两种模式。
Sources: MCDCmdTrans.cpp
核心算法:calcKey()
calcKey() 是整个 SeedToKey 体系的核心——它接收 8 字节输入,经过位运算迭代后输出 24 位密钥。该函数在 algorithm.cpp 和 MCDCmdTrans.cpp 中各有一份完全相同的实现(分别命名为 calcKey 和 Calc_Key)。
flowchart LR
A["8字节输入 inBuffer[8]"] --> B["初始化状态 v3 = 0xC541A9"]
B --> C{"外层循环: v4 = 0..7"}
C --> D["读取字节 v5 = inBuffer[v4]"]
D --> E{"内层循环: v6 = 0..7 (逐bit)"}
E --> F{"v5 最高位 ≠ v3 最高位?"}
F -->|是| G["v3 |= 0x1000000<br/>v7 = 1085480"]
F -->|否| H["v7 = 0"]
G --> I["v5 右移 1 位"]
H --> I
I --> J["v8 = (v3>>1 ^ v7) & 0x109028"]
J --> K["v3 = (v3>>1 & 0xFFEF6FD7 | v8) & 0xFFFFFF"]
K --> E
K -->|8次后| C
C -->|8字节后| L["提取 3 字节结果<br/>result = v2[0]<<16 | v2[1]<<8 | v2[2]"]
L --> M["返回 24-bit key"]算法关键特征:
| 特征 | 值 | 说明 |
|---|---|---|
| 初始状态 | 0xC541A9 | 24 位 LFSR 初始种子 |
| 反馈多项式掩码 | 0x109028 | 位变换的核心常量 |
| 保持位掩码 | 0xFFEF6FD7 | 保护不需要翻转的位 |
| 输入长度 | 固定 8 字节 | 超出部分不参与计算 |
| 输出长度 | 3 字节(24 位) | 大端序排列 |
| 条件触发常量 | 0x1000000 / 1085480 | 输入位与状态位不同时触发 |
本质上这是一个带条件反馈的 24 位 LFSR(线性反馈移位寄存器),通过 8 个输入字节的 64 个 bit 逐位驱动状态转移,最终从终态中提取出 3 字节密钥。
Sources: algorithm.cpp, MCDCmdTrans.cpp
seedToKey():C API 入口
seedToKey() 是暴露给 OTX 运行时的 C 语言接口,负责将 Seed 和 Mask 拼接后送入 calcKey():
seedToKey(binSeed, seedLen, binMask, maskLen, outBuffer, outBufferSize)
├── 校验: seedLen + maskLen < 1024
├── 拼接: tempBuffer = binSeed + binMask
├── 计算: dwKey = calcKey(tempBuffer, seedLen + maskLen)
└── 拆解: outBuffer[0..2] = (dwKey >> 16), (dwKey >> 8), (dwKey)函数返回固定值 3(表示输出 3 字节),出错返回 -1。内部使用 std::recursive_mutex 保证线程安全——这与算法集中其他加密函数(AES、DES、SHA256)保持一致。
Sources: algorithm.cpp
MCDCmdTrans::SeedToKey():刷写管线版本
MCDCmdTrans 中封装了一个 CBinary 版本的 SeedToKey(),逻辑与 C API 一致但使用 CBinary 作为数据容器:
MCDCmdTrans::SeedToKey(CBinary binSeed, CBinary binMask)
├── binData = binSeed + binMask // CBinary 重载的 operator+
├── dwKey = Calc_Key(binData.GetBuffer())
└── 返回 CBinary{key[0], key[1], key[2]}值得注意的是,该方法内部包含 _VM_PROTECT_ 条件编译块——在开启虚拟机保护的 Windows 构建中,密钥计算过程会被额外的代码混淆保护包裹。
Sources: MCDCmdTrans.cpp
SecurityAccess():完整的 UDS 0x27 流程
MCDCmdTrans::SecurityAccess() 是按安全等级分派的完整安全访问实现:
sequenceDiagram
participant T as 诊断仪
participant E as ECU
Note over T,E: SecurityAccess(pinCode, iLevel)
T->>E: 0x27 [oddLevel] (Request Seed)
E-->>T: 0x67 [oddLevel] [Seed...]
alt Seed 全零
T-->>T: 已解锁,直接返回 true
else iLevel == 7
T-->>T: Key = 固定Mask (16字节)
else iLevel == 4/11/2
T-->>T: Mask = 0xFFFFFFFFFF
T-->>T: Key = SeedToKey(Seed, Mask)
else 其他Level
T-->>T: Mask = hexDecode(pinCode)
T-->>T: Key = SeedToKey(Seed, Mask)
end
T->>E: 0x27 [evenLevel] [Key...]
E-->>T: 0x67 [evenLevel] (成功) 或 0x7F 0x27 [NRC]安全等级与 Mask 策略对照表:
| Level | Request SID | Send SID | Mask 来源 | 说明 |
|---|---|---|---|---|
| 1 | 0x27 0x03 | 0x27 0x04 | pinCode hex 解码 | 标准安全访问 |
| 2 | 0x27 0x01 | 0x27 0x02 | 0xFFFFFFFFFF (5B) | 最常用:普通刷写/换件 |
| 3 | 0x27 0x05 | 0x27 0x06 | pinCode hex 解码 | 高权限操作 |
| 4 | 0x27 0x01 | 0x27 0x02 | 0xFFFFFFFFFF (5B) | 换件刷写优先 |
| 7 | 0x27 0x07 | 0x27 0x08 | 0x001122...EEFF (16B) | 特殊 ECU,直接使用 Mask 作为 Key |
| 11 | 0x27 0x11 | 0x27 0x12 | 0xFFFFFFFFFF (5B) | 扩展安全访问 |
当 iLevel == 7 时,不走 SeedToKey() 计算——直接使用 16 字节固定 Mask 作为 Key。这对应于某些 ECU 特定的安全策略。
对于错误响应 0x7F 0x27 0x36(超过尝试次数限制),代码会等待 10 秒;对于 0x7F 0x27 0x35(密钥无效),等待 2 秒后返回失败。
Sources: MCDCmdTrans.cpp
SecurityAccess_ECOS():ECOS 变体
SecurityAccess_ECOS() 是 SecurityAccess() 的简化变体,主要用于 ECOS(ECU 操作系统)场景。其核心区别在于:始终使用 pinCode 作为 Mask,不支持 Level 7 的固定 Mask 策略。其余流程(Seed 请求、Key 计算、Key 发送、错误处理)完全一致。
Sources: MCDCmdTrans.cpp
Get_SecurityMask():ECU 级 Mask 映射表
每个 ECU 实例都有一个通过 DoIP 地址索引的唯一 5 字节 Mask,存储在 Get_SecurityMask() 的静态映射表中:
默认值: {0x41, 0xAA, 0x42, 0xBB, 0x43}
部分映射:
DoIP 0x1001 → {0x19, 0xB8, 0x34, 0xDC, 0xA2} (BGM)
DoIP 0x1201 → {0x71, 0xCF, 0x05, 0xA2, 0xED} (CDC)
DoIP 0x1401 → {0xA5, 0x79, 0xF8, 0xED, 0xC1} (ACU)
DoIP 0x1011 → {0xF5, 0x6B, 0x94, 0xCD, 0x8A} (TCAM)
DoIP 0x1635 → {0x87, 0x5C, 0x0E, 0xB1, 0x10} (BECM1)
...此映射表覆盖了项目中定义的全部 50+ ECU 类型。每条记录对应 jidu.cpp 中 ecutable[] 数组的一个 ECU 节点,确保每个 ECU 拥有独立的密钥派生参数。
Sources: MCDCmdTrans.cpp, jidu.cpp
刷写管线中的调用链
BinFileDownloader 是 SeedToKey 的主要调用者。在域控制器(TCAM、BGM、CDC、ACU)的刷写流程中,安全访问按以下逻辑执行:
BinFileDownloader::downloadFile()
└── if (ecu->getSecurityAccess()) // ECU 配置了安全访问
├── cmdTrans->EnterSystem() // 进入扩展会话
└── if (securityAccessMode == 0)
├── SecurityAccess_ECOS(pinCode) // 优先 ECOS 方式
└── [失败] SecurityAccess("FFFFFFFF", 2) // 回退换件密钥
else (securityAccessMode == 1)
├── SecurityAccess("FFFFFFFF", 2) // 优先换件密钥
└── [失败] SecurityAccess_ECOS(pinCode) // 回退 ECOS两种模式的根本区别:Mode 0 适用于普通刷写(优先使用车辆订单中的 pinCode),Mode 1 适用于换件刷写(优先使用 FFFFFFFF 通用密钥)。双路径回退机制确保了不同场景下的兼容性。
VbfFileDownloader 不直接调用 SecurityAccess()——它的安全验证通过 VerifyAuthenticity() 完成签名校验,SeedToKey 在其刷写管线中不出现。
Sources: BinFileDownloader.cpp, ECUFlasherImpl.cpp
CRC16-IBM:CA 证书数据的完整性校验
算法实现
crc16_ibm() 实现了标准的 CRC-16-IBM(又名 CRC-16-ANSI、CRC-16-REFIN/REFOUT),其参数为:
| 参数 | 值 | 说明 |
|---|---|---|
| 多项式 (Polynomial) | 0x8005 | x¹⁶ + x¹⁵ + x² + 1 |
| 初始值 (Init) | 0x0000 | 寄存器初始为零 |
| 输入反射 (RefIn) | true | 每个字节先做位反转 |
| 输出反射 (RefOut) | true | 最终结果做位反转 |
| 异或输出 (XorOut) | 0x0000 | 不额外异或 |
算法流程:
crc16_ibm(inParams, inLen)
wCRCin = 0x0000
for each byte:
wChar = InvertUint8(byte) // 位反转
wCRCin ^= (wChar << 8) // 异或到高位
for 8 bits:
if wCRCin & 0x8000: // 最高位为1
wCRCin = (wCRCin << 1) ^ 0x8005
else:
wCRCin = wCRCin << 1
return InvertUint16(wCRCin) // 输出位反转辅助函数 InvertUint8 和 InvertUint16 执行位级镜像反转——例如 0x80 (10000000) 反转为 0x01 (00000001)。这种"双反射"设计使 CRC16-IBM 与常见硬件 CRC 实现完全一致。
Sources: algorithm.cpp
唯一调用场景:sendCAData()
CRC16-IBM 在整个项目中的唯一调用者是 sendCAData() 函数。该函数负责将 ECU 的 CA(Certificate Authority)证书数据打包为传输格式:
sendCAData(binGetPKI, iLen, bKeyId, outBuffer, outBufferSize)
├── 拷贝证书数据到 outBuffer[0..iLen-1]
├── 填充 0xFF 至 outBuffer[iLen..2043] // 固定 2044 字节
├── CRC = crc16_ibm(outBuffer, 2044) // 对整个 2044 字节计算 CRC
├── outBuffer[2044] = CRC 高字节
├── outBuffer[2045] = CRC 低字节
├── memmove: 整体右移 3 字节 (为头部腾空间)
├── outBuffer[0] = bKeyId // 密钥 ID
├── outBuffer[1..2] = 原始数据长度 (大端) // 有效载荷长度
└── 返回 2049 (2044 + 2 CRC + 3 Header)数据包结构(共 2049 字节):
┌─────────┬─────────┬──────────────────────┬──────────┐
│ bKeyId │ Payload │ PKI Data + 0xFF │ CRC16-IBM│
│ (1Byte) │ Len (2B)│ Padding (2044B) │ (2B) │
└─────────┴─────────┴──────────────────────┴──────────┘CRC 覆盖的是填充后的完整 2044 字节块(原始证书 + 0xFF 填充),而非仅覆盖有效载荷。这种设计确保接收方可以验证整个传输块的完整性,包括填充区域。
Sources: algorithm.cpp
安全常量与 pinCode 来源
SeedToKey 计算所需的 pinCode(即 Mask 的来源之一)并非硬编码,而是通过多层数据通道获取:
pinCode 获取路径(优先级由 SecurityAccessMode 决定):
Mode 0 (普通刷写):
车辆订单 pinCode → ECU 安全常量 keySwdl → "FFFFFFFFFF" 回退
Mode 1 (换件刷写):
"FFFFFFFFFF" → ECU 安全常量 keySwdl 回退JD_ECU_SAFETY_CONSTANT_t 结构体包含四类密钥:keySwdl(软件下载密钥,SeedToKey 使用此项)、keyImmo(防盗密钥)、keySal、keyCommon。这些常量由 DataCenter 通过云端接口获取并缓存,详见 DataCenter:车辆订单、安全常量、证书信息的统一缓存中心。
Sources: jidu_macro.h, jidu_flasher.cpp
线程安全与并发保障
所有位于 algorithm.cpp 中的加密函数(含 seedToKey 和 crc16_ibm)均使用同一个 static std::recursive_mutex 进行保护。选择 recursive_mutex 而非普通 mutex 的原因在于:部分函数之间存在内部调用关系(例如 sendCAData 内部调用 crc16_ibm),递归锁避免了同一线程的重入死锁。
MCDCmdTrans::SeedToKey() 和 MCDCmdTrans::Calc_Key() 则未加锁——它们的调用者 SecurityAccess() 运行在串行刷写管线的上下文中,由 ParallelECUFlasher 的 TaskPool 保证每个 ECU 链路独立串行执行,因此无需额外同步。
Sources: algorithm.cpp
与其他模块的关系
- 加密算法集:SeedToKey 与 CRC16 同属 algorithm.cpp 算法集。该文件还包含 AES、DES-CBC、SHA256、ECDSA 签名、Base64 等——完整加密图谱的组成部分
- MCDFileDownloader:MCDCmdTrans 创建的 DoIP 诊断通道承载了 SecurityAccess 的 UDS 通信
- BinFileDownloader 与 VbfFileDownloader:BinFileDownloader 直接调用 SecurityAccess;VbfFileDownloader 通过 VerifyAuthenticity 走签名验证路径
- DataCenter:提供 pinCode 和安全常量数据源
- 刷写器架构总览:SecurityAccess 在整个刷写层级中的位置
设计要点总结
双实现并存:
algorithm.cpp中的 C API(seedToKey)与MCDCmdTrans中的 C++ 封装(SeedToKey)各自独立维护相同的calcKey/Calc_Key逻辑。任何算法变更需同步两处。ECU 级差异化 Mask:
Get_SecurityMask()为每个 ECU 提供独立的 5 字节 Mask,加上可变 pinCode,形成两级密钥派生参数体系。CRC16 的单一用途:与 SeedToKey 的广泛使用不同,CRC16-IBM 仅服务于
sendCAData()的证书传输场景,职责清晰且边界明确。安全等级的灵活路由:7 个安全等级 + 2 种刷写模式 + 双路径回退,构成了一套覆盖普通刷写、换件刷写、特殊 ECU 等多种场景的完整安全访问策略。