Skip to content

本文档剖析 otxproxy 插件中实现的两项诊断安全核心算法:SeedToKey(UDS 安全访问密钥派生)与 CRC16-IBM(CA 证书数据的完整性校验)。两者均通过 algorithm.hOTX_API 声明对外暴露,供 OTX 运行时和 ECU 刷写管线调用。

整体架构概览

SeedToKey 和 CRC16 虽然同为底层安全算法,但在系统中的角色截然不同。SeedToKey 是 ECU 刷写安全访问(Security Access)协议层的核心——它根据 ECU 返回的 Seed 和预置的 Mask 计算出解锁密钥,是刷写流程的前置必要条件。CRC16-IBM 则独立服务于证书数据传输场景,负责计算 CA 证书数据的 16 位校验值。

mermaid
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 进行身份验证。标准流程分为两步:

  1. 请求种子 (Request Seed):诊断仪发送 0x27 0x01(Level 1 示例),ECU 返回 0x67 0x01 [Seed]
  2. 发送密钥 (Send Key):诊断仪计算密钥后发送 0x27 0x02 [Key],ECU 验证通过后解锁

otxproxy 实现了多种安全等级(Level 1–11),覆盖普通刷写(pinCode)和换件刷写(FFFFFFFF)两种模式。

Sources: MCDCmdTrans.cpp

核心算法:calcKey()

calcKey() 是整个 SeedToKey 体系的核心——它接收 8 字节输入,经过位运算迭代后输出 24 位密钥。该函数在 algorithm.cppMCDCmdTrans.cpp 中各有一份完全相同的实现(分别命名为 calcKeyCalc_Key)。

mermaid
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"]

算法关键特征:

特征说明
初始状态0xC541A924 位 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() 是按安全等级分派的完整安全访问实现:

mermaid
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 策略对照表:

LevelRequest SIDSend SIDMask 来源说明
10x27 0x030x27 0x04pinCode hex 解码标准安全访问
20x27 0x010x27 0x020xFFFFFFFFFF (5B)最常用:普通刷写/换件
30x27 0x050x27 0x06pinCode hex 解码高权限操作
40x27 0x010x27 0x020xFFFFFFFFFF (5B)换件刷写优先
70x27 0x070x27 0x080x001122...EEFF (16B)特殊 ECU,直接使用 Mask 作为 Key
110x27 0x110x27 0x120xFFFFFFFFFF (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.cppecutable[] 数组的一个 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)0x8005x¹⁶ + 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)         // 输出位反转

辅助函数 InvertUint8InvertUint16 执行位级镜像反转——例如 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(防盗密钥)、keySalkeyCommon。这些常量由 DataCenter 通过云端接口获取并缓存,详见 DataCenter:车辆订单、安全常量、证书信息的统一缓存中心

Sources: jidu_macro.h, jidu_flasher.cpp

线程安全与并发保障

所有位于 algorithm.cpp 中的加密函数(含 seedToKeycrc16_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 在整个刷写层级中的位置

设计要点总结

  1. 双实现并存algorithm.cpp 中的 C API(seedToKey)与 MCDCmdTrans 中的 C++ 封装(SeedToKey)各自独立维护相同的 calcKey/Calc_Key 逻辑。任何算法变更需同步两处。

  2. ECU 级差异化 MaskGet_SecurityMask() 为每个 ECU 提供独立的 5 字节 Mask,加上可变 pinCode,形成两级密钥派生参数体系。

  3. CRC16 的单一用途:与 SeedToKey 的广泛使用不同,CRC16-IBM 仅服务于 sendCAData() 的证书传输场景,职责清晰且边界明确。

  4. 安全等级的灵活路由:7 个安全等级 + 2 种刷写模式 + 双路径回退,构成了一套覆盖普通刷写、换件刷写、特殊 ECU 等多种场景的完整安全访问策略。