本文档系统性地剖析 OTX 代理插件中所有加密算法、编码工具和辅助运算函数的实现原理与调用关系。主要包括:Base64 编解码、SHA256 哈希、DES-CBC 加解密(PKCS5 填充)、AES ECB(纯软件实现)与 AES-CBC(OpenSSL)、ECDSA 签名、CRC16-IBM 校验、进制转换工具(Hex ↔ ByteField)、SeedToKey 密钥计算,以及刷写文件专用的 RSA+AES-CBC 双层解密流水线。
模块架构总览
整个加密算法体系由三层构成:底层纯软件实现层(AES 类、CRC16 位反转算法)、OpenSSL 封装层(SHA256、DES-CBC、ECDSA、RSA、AES-CBC)、OTX API 导出层(algorithm.h 中通过 extern "C" 暴露给 OTX 运行时的所有函数)。其中 FlashFileUtils 是一个独立的消费者模块,它将 Base64 解码、RSA 私钥解密、AES-CBC 解密组合成一条完整的流水线,用于解密被加密的 VBF 刷写文件和配置文件。
graph TB
subgraph OTX_API["OTX API 导出层 (algorithm.h)"]
b64e["base64_encode / base64_decode"]
sha["sha256"]
des["des_cbc_pkcs5_encrypt / decrypt"]
aes["aes_cipher"]
ecdsa["ecdsa_signature"]
crc["crc16_ibm"]
radix["radix_hex2Array / array2Hex / ..."]
stk["seedToKey / ver_calc"]
uuid["getUUID / pow_calc / sendCAData"]
end
subgraph OpenSSL["OpenSSL 封装层"]
ossl_sha["SHA256_CTX"]
ossl_des["DES_ncbc_encrypt"]
ossl_ec["ECDSA_sign / EC_KEY"]
ossl_rsa["RSA_private_decrypt"]
ossl_aes_cbc["AES_set_decrypt_key / AES_cbc_encrypt"]
ossl_p12["PKCS12_parse / PEM"]
end
subgraph PureSW["纯软件实现层"]
aes_class["AES 类 (aes.cpp)"]
crc_algo["CRC16-IBM 位反转"]
end
subgraph Consumers["消费者模块"]
flash["FlashFileUtils<br/>RSA→AES-CBC 流水线"]
jidu_client["JiduClient (ECDSA 签名)"]
vbf_parser["VBFParserSmall"]
mcd_trans["MCDCmdTrans"]
end
b64e --> flash
ossl_rsa --> flash
ossl_aes_cbc --> flash
aes_class --> aes
crc_algo --> crc
ossl_sha --> sha
ossl_des --> des
ossl_ec --> ecdsa
ossl_p12 --> ossl_rsa
flash --> vbf_parser
ecdsa --> jidu_client模块依赖关系:algorithm.h 被 15 个源文件引用,是整个系统的密码学基础设施。其中 FlashFileUtils 使用了 base64_decode(来自 algorithm.h)和 OpenSSL 的 RSA/AES-CBC(直接调用),构成刷写文件解密的完整链路。
Sources: algorithm.h, algorithm.cpp, FlashFileUtils.cpp, FlashFileUtils.h
Base64 编解码
Base64 是 OTX 系统中使用最广泛的编码工具,用于将二进制密钥、签名数据、证书内容转换为可安全嵌入 JSON 传输的文本格式。实现为纯 C 算法,不依赖任何第三方库。
编码过程以 3 字节为一组,分解为 4 个 6 位索引,查表输出对应字符。编码表为标准 Base64 字母表:A-Za-z0-9+/。当输入长度不是 3 的倍数时,末尾补 = 号——余 1 字节时补两个 =,余 2 字节时补一个 =。输出长度计算遵循公式:若 str_len % 3 == 0,则 len = str_len / 3 * 4;否则 len = (str_len / 3 + 1) * 4。
flowchart LR
subgraph Input["3 字节输入"]
B0["Byte0<br/>b7..b0"]
B1["Byte1<br/>b7..b0"]
B2["Byte2<br/>b7..b0"]
end
subgraph Split["6位分组"]
G0["b7..b2"]
G1["b1..b0 + b7..b4"]
G2["b3..b0 + b7..b6"]
G3["b5..b0"]
end
subgraph Output["4 字符输出"]
C0["table[G0]"]
C1["table[G1]"]
C2["table[G2]"]
C3["table[G3]"]
end
B0 --> G0 & G1
B1 --> G1 & G2
B2 --> G2 & G3
G0 --> C0
G1 --> C1
G2 --> C2
G3 --> C3解码过程使用一个 128 字节的查找表,将 ASCII 字符直接映射为 0-63 的数值(非法字符映射为 0)。通过检测末尾 = 号数量来还原原始长度:== 则减 2 字节,单个 = 则减 1 字节。每次读取 4 个字符,还原出 3 个字节。
| 特性 | 细节 |
|---|---|
| 编码表 | ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ |
| 填充字符 | = |
| 输入单位 | 3 字节 → 4 字符 |
| 线程安全 | std::recursive_mutex 保护 |
| 错误处理 | 输出缓冲区不足时返回 -1,并通过 Tracer 记录错误 |
在 FlashFileUtils::DecryptConfig 中,Base64 解码作为流水线的第一环——将文本形式的加密配置还原为二进制,再送入 AES-CBC 解密。
Sources: algorithm.cpp
SHA256 哈希
SHA256 哈希通过 OpenSSL 的低级 API(SHA256_CTX)实现,遵循标准的 Init → Update → Final 三段式调用模式。输出固定为 32 字节(256 位)的二进制摘要。
sequenceDiagram
participant Caller as 调用方
participant SHA as sha256()
participant CTX as SHA256_CTX
Caller->>SHA: inParams, inLen, outBuffer, outBufferSize
SHA->>SHA: lock(m_mutex)
SHA->>SHA: 检查 outBufferSize >= 32
SHA->>CTX: SHA256_Init(&context)
SHA->>CTX: SHA256_Update(&context, inParams, inLen)
SHA->>CTX: SHA256_Final(outBuffer, &context)
SHA-->>Caller: 返回 32(成功)或 -1(缓冲区不足)SHA256 在整个系统中承担两个角色:其一作为独立的哈希函数供 OTX 脚本直接调用;其二在 ECDSA 签名流程中被用作消息摘要算法——ecdsa_signature() 内部先对消息执行 SHA256() 计算哈希,再将摘要传递给 ECDSA_sign() 进行签名。
Sources: algorithm.cpp, algorithm.cpp
DES-CBC 加密与解密(PKCS5 填充)
DES-CBC 模块提供对称加密能力,使用 8 字节密钥和 固定 IV 向量,采用 CBC(密码块链接)模式,并自行实现了 PKCS5Padding(与 PKCS7Padding 等效,仅填充范围限定为 8 字节块)。
加密过程
flowchart TD
PT["明文 (clearText)"] --> Split["按 8 字节分块"]
Split --> Full["完整块 (clearLen/8 个)"]
Split --> Rem["剩余字节 (clearLen%8)"]
Full --> EncLoop["DES_ncbc_encrypt 循环<br/>每块加密后 IV=密文块"]
Rem --> Pad["PKCS5 填充<br/>填充值 = 8 - 余数"]
Pad --> FinalEnc["最终块加密"]
EncLoop --> Merge["拼接所有密文块"]
FinalEnc --> Merge
Merge --> CT["密文输出"]PKCS5 填充规则:若明文长度为 8 的整数倍,追加一个完整的 8 字节块,每字节值为 0x08;若余 r 字节(r < 8),则追加 8 - r 个值为 8 - r 的字节。
固定 IV 向量(加密):{'j', 'k', 't', '1', '2', '3', '4', '5'}。
密钥处理:若传入的密钥字符串长度超过 8 字节,只截取前 8 字节;若不足 8 字节,用 0x00 填充至 8 字节。使用 DES_set_key_unchecked 设置密钥表(不检查奇偶性)。
解密过程
解密端 IV 向量为:{'s', 'i', 'u', 'h', 'u', 'a', 'l', 'i'}(与加密端的 IV 不同),采用逐块解密的方式,支持处理非 8 字节对齐的输入。
| 参数 | 加密 | 解密 |
|---|---|---|
| IV 向量 | jkt12345 | siuhuali |
| 模式 | CBC | CBC |
| 填充 | PKCS5(自行实现) | PKCS5(需调用方处理) |
| 密钥长度 | 最多 8 字节(截断或补零) | 最多 8 字节(截断或补零) |
| 线程安全 | recursive_mutex | recursive_mutex |
Sources: algorithm.cpp
AES 加密——纯软件实现与 OpenSSL 封装双轨
OTX 系统同时存在两套 AES 实现,服务于不同场景:
| 维度 | AES 类(aes.cpp) | OpenSSL AES-CBC(FlashFileUtils) |
|---|---|---|
| 模式 | ECB(电子密码本) | CBC(密码块链接) |
| 实现方式 | 纯 C++ 软件实现(S-Box 查表) | 调用 OpenSSL AES_cbc_encrypt |
| 密钥长度 | 128 位(10 轮) | 128 位 |
| IV 向量 | 无 | 有(硬编码或派生自 RSA 解密结果) |
| 使用场景 | OTX API aes_cipher() 单块加密 | 刷写文件/配置文件/私钥文件批量解密 |
AES 纯软件实现(aes.cpp)
AES 类完整实现了 AES-128 标准的全部四个变换:SubBytes(S-Box 替换)、ShiftRows(行移位)、MixColumns(列混合,使用 FFmul 伽罗瓦域乘法)、AddRoundKey(轮密钥加)。加密为 10 轮(第 10 轮省略 MixColumns),解密为逆序的 10 轮。
flowchart TD
subgraph Encrypt["加密流程"]
E0["输入 16 字节"] --> E1["AddRoundKey(w[0])"]
E1 --> E2["Round 1-9:<br/>SubBytes→ShiftRows→MixColumns→AddRoundKey"]
E2 --> E3["Round 10:<br/>SubBytes→ShiftRows→AddRoundKey"]
E3 --> E4["输出 16 字节"]
end
subgraph KeyExp["密钥扩展"]
K0["128-bit Key"] --> K1["KeyExpansion → w[11][4][4]"]
end
K1 --> E1 & E2 & E3KeyExpansion 使用 Rcon 常量数组 {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36} 迭代生成 11 轮轮密钥(每轮 4×4 字节)。
Cipher(void* input, int length) 重载支持多块加密:每次处理 16 字节,若 length=0 则以 \0 结尾的字符串自动计算长度。
OTX API 封装 aes_cipher()
aes_cipher() 将输入数据截断为最多 16 字节(ECB 单块),使用调用方提供的密钥构造 AES 对象并加密,返回固定 16 字节输出。
int aes_cipher(unsigned char* input, int inLen, unsigned char* key, unsigned char* outBuffer);
// 返回: 固定 16(成功)FlashFileUtils 中的 AES-CBC
FlashFileUtils 使用 OpenSSL 的 AES_cbc_encrypt 实现 CBC 模式解密,共有三套密钥配置:
| 函数 | 密钥(16 字节) | IV(16 字节) | 用途 |
|---|---|---|---|
AES_DecryptRSAPrivateKey | 0xAE,0x15,0x62,0x75,0x67,0xBC,0xA2,0x98,0x23,0x11,0xDC,0xF1,0xFA,0xAA,0xBC,0x54 | debug0000000000(hex) | 解密 RSA 私钥文件 |
AES_DecryptConfig | jiduautojiduauto | 全零 IV | 解密配置文件 |
AES_DecryptEx | 派生自 RSA 解密结果 m_Key[0..15] | debug0000000000(hex) | 解密 VBF 刷写文件 |
其中 AES_DecryptEx 的密钥来自 RSA 解密后的前 16 字节,构成了 "RSA 解密 → 获取 AES 密钥 → AES-CBC 解密" 的双层保护链。
Sources: aes.h, aes.cpp, algorithm.cpp, FlashFileUtils.cpp
ECDSA 签名
ecdsa_signature() 为集度云端服务的请求提供身份认证签名。签名流程分三步:从 PEM 格式字符串读取 EC 私钥 → 对消息计算 SHA256 哈希 → 使用 ECDSA 签名并 Base64 编码。
sequenceDiagram
participant Client as JiduClient::callServer
participant ECDSA as ecdsa_signature()
participant OpenSSL as OpenSSL
participant B64 as base64_encode
Client->>ECDSA: msg=plainMsg, key=keyPem
ECDSA->>OpenSSL: PEM_read_bio_ECPrivateKey(keyPem)
OpenSSL-->>ECDSA: EC_KEY*
ECDSA->>OpenSSL: SHA256(msg) → hash[32]
ECDSA->>OpenSSL: ECDSA_sign(0, hash, 32, sig, &sig_len, ekey)
OpenSSL-->>ECDSA: sig[128], sig_len
ECDSA->>B64: base64_encode(sig, sig_len)
B64-->>ECDSA: Base64 签名字符串
ECDSA-->>Client: signature (Base64)调用上下文(jidu_client.cpp)中,签名的明文消息由以下字段拼接而成:
plainMsg = inParams + url + method + serviceName + timestamp五个字段分别对应:请求体 JSON、API 路径、HTTP 方法、服务名 "diagnostic-adapter-service"、Unix 秒级时间戳。签名输出经 Base64 编码后,以 "signature" 字段嵌入代理请求 JSON,随 serialNo(设备序列号)一同发往集度服务器进行身份验证。
Sources: algorithm.cpp, jidu_client.cpp
CRC16-IBM 校验
crc16_ibm() 实现 CRC-16-IBM(多项式 0x8005,即 x¹⁶ + x¹⁵ + x² + 1),采用位反转的处理方式:输入每个字节先做位反转,CRC 寄存器初始化为 0x0000,最终输出再做一次 16 位反转。
flowchart LR
subgraph PerByte["逐字节处理"]
B["byte → InvertUint8"] --> XOR["CRC ^= (byte << 8)"]
XOR --> Loop["8 次迭代"]
Loop --> Check{"CRC & 0x8000 ?"}
Check -->|Yes| S1["CRC = (CRC << 1) ^ 0x8005"]
Check -->|No| S2["CRC = CRC << 1"]
S1 --> Loop
S2 --> Loop
end
Init["CRC = 0x0000"] --> PerByte
PerByte --> Final["InvertUint16(CRC)"]
Final --> Result["返回 CRC16 值"]该函数在 sendCAData() 中被用于计算 CA 证书数据的完整性校验——对 2044 字节的数据计算 CRC16,将结果附在末尾,再加上头部(KeyId + 长度),最终封装为 2049 字节的诊断消息帧。
Sources: algorithm.cpp, algorithm.cpp
进制转换工具集(Radix Functions)
algorithm.h 提供了一组完整的进制转换函数,用于在 OTX 脚本中实现 Hex 字符串、ByteField(字节数组)、整数、十进制字符串之间的相互转换。
| 函数 | 输入 → 输出 | 关键特性 |
|---|---|---|
radix_hex2Array | Hex 字符串 → 字节数组 | 自动去除 0x/0X 前缀,奇数长度前补 0 |
radix_array2Hex | 字节数组 → 大写 Hex 字符串 | 逐字节转换为两个 Hex 字符 |
radix_array2Uint32 | 大端字节数组 → uint32 | 支持不足 4 字节的左对齐解析 |
radix_hex2Integer | Hex 字符串 → 整数 | 调用 MyStringUtils::toLong(src, radix) |
radix_string2Integer | 任意进制字符串 → 整数 | 包装 MyStringUtils::toLong |
radix_int2ByteArray | uint32 → 大端 4 字节数组 | 固定输出 4 字节 |
radix_decimalStringToHex | 十进制字符串 → 大写 Hex 字符串 | 使用 std::stoll + std::hex |
appendBytefield | 两段字节数组拼接 | 直接 memcpy 拼接 |
所有 radix 函数均受 recursive_mutex 保护,且在缓冲区不足时返回 -1。
Sources: algorithm.cpp
SeedToKey 密钥计算
seedToKey() 实现诊断安全访问(Security Access)中的密钥计算。它接收 ECU 下发的随机种子(Seed)和掩码(Mask),拼接后送入 calcKey() 进行自定义算法计算,输出 3 字节密钥。
calcKey() 是一个基于 24 位状态寄存器和8 轮位迭代的确定性变换。其核心逻辑为:
- 初始化状态
v3 = 0xC541A9(24 位常量) - 对输入的前 8 个字节,每个字节执行 8 次迭代:
- 若当前输入 bit 的 MSB 与状态寄存器 MSB 不同,则将状态寄存器第 24 位置 1 并引入修正值
0x109028 - 状态寄存器右移一位,与修正值做位与/位或混合操作
- 最终保留低 24 位
- 若当前输入 bit 的 MSB 与状态寄存器 MSB 不同,则将状态寄存器第 24 位置 1 并引入修正值
- 从最终状态中提取 3 字节作为密钥输出
| 步骤 | 说明 |
|---|---|
| 输入 | binSeed[seedLen] + binMask[maskLen] 拼接 |
| 拼接总长度限制 | < 1024 字节 |
| 输出格式 | 3 字节(大端序) |
| 内部状态 | 24 位寄存器 |
Sources: algorithm.cpp
刷写文件解密流水线(FlashFileUtils)
FlashFileUtils 实现了刷写文件(VBF)和加密配置文件的解密流水线,是 RSA + AES-CBC 双层保护的完整实现。
flowchart TD
subgraph Stage1["第一层:RSA 解密"]
K1["加密的 AES 密钥<br/>(Base64 字符串)"]
K1 --> B64D["base64_decode"]
B64D --> K1BIN["二进制密文"]
K1BIN --> RSAD["RSA_private_decrypt<br/>(PKCS1_PADDING)"]
RSAD --> AESKEY["AES 密钥 m_Key[1024]<br/>取前 16 字节"]
end
subgraph Stage2["第二层:AES-CBC 解密"]
VBF["加密的 VBF 文件<br/>(跳过 0x40 头部)"]
VBF --> AESD["AES_cbc_encrypt<br/>key=m_Key[0..15]<br/>iv=debug0000000000"]
AESD --> PKCS7["DePKCS7Padding"]
PKCS7 --> PLAIN["明文 VBF 数据"]
end
Stage1 --> Stage2RSA 私钥文件本身也是加密存储的:先通过 AES_DecryptRSAPrivateKey(使用硬编码密钥)解密私钥文件,再将其解析为 RSA* 对象。InitRSA 方法接收 Base64 编码的加密 AES 密钥,经过 base64 解码 → RSA 私钥解密 → 得到明文 AES 密钥,存入 m_Key。
DecryptFlashFile 使用 m_Key 的前 16 字节作为 AES-128 密钥,配合固定 IV debug0000000000(hex),对跳过 0x40 字节头部的 VBF 文件执行 AES-CBC 解密,最后通过 DePKCS7Padding 去除填充。
Sources: FlashFileUtils.cpp, FlashFileUtils.h
其他辅助函数
ver_calc —— 版本号计算
根据 iType 参数支持 5 种版本号格式化模式:5 字节 BCD + 3 字节 ASCII(两种偏移)、纯 Hex、ASCII 码过滤(截断于 0x00/0xFF)、1 + N×13 分组 ASCII(逗号分隔)。输出经过字符白名单过滤(仅保留 0x2D-0x7A 和 0x20)。
sendCAData —— CA 证书数据封装
将 PKI 数据填充至 2044 字节(不足部分填 0xFF),计算 CRC16-IBM 校验和,附加 3 字节头部(KeyId + 大端长度),返回 2049 字节的诊断消息帧。
getUUID / pow_calc / utf8toGbk
- getUUID:使用
sole::uuid4()生成随机 UUID v4,去除连字符后输出 32 字符十六进制字符串 - pow_calc:计算
base^exponent - 1并转为十进制字符串 - utf8toGbk:调用
MyStringUtils::UTF8ToGBK进行字符编码转换
ext_printf / ext_strcmp / ext_strncmp / ext_replace
这四个函数是标准 C 库函数的线程安全包装:ext_printf 使用 vsnprintf + va_list 实现格式化输出;ext_replace 调用 MyStringUtils::replace 进行子串替换。
Sources: algorithm.cpp, algorithm.cpp
调用关系与数据流总结
flowchart LR
subgraph OTX_Runtime["OTX 运行时脚本"]
S1["调用 base64_encode"]
S2["调用 sha256"]
S3["调用 aes_cipher"]
S4["调用 des_cbc_pkcs5_encrypt"]
S5["调用 radix_hex2Array"]
S6["调用 seedToKey"]
end
subgraph JiduClient["JiduClient"]
JC["ecdsa_signature<br/>签名请求消息"]
end
subgraph Flasher["刷写模块"]
FL["InitRSA → DecryptFlashFile<br/>双层解密流水线"]
end
subgraph Core["algorithm.cpp 核心"]
CORE["所有 OTX_API 函数"]
end
S1 & S2 & S3 & S4 & S5 & S6 --> CORE
JC --> CORE
FL -->|base64_decode| CORE
FL -->|OpenSSL RSA/AES| FL所有 extern "C" 导出的函数均使用全局 std::recursive_mutex 保证线程安全(可重入锁,避免同一线程内嵌套调用时的死锁)。AES 纯软件实现本身非线程安全(操作同一状态数组),但 aes_cipher() 在每次调用时构造局部 AES 对象,自然规避了竞争条件。
Sources: algorithm.h, algorithm.cpp
后续阅读建议
本文档覆盖了加密算法与编码工具的全部实现细节。建议按以下路径深入理解相关模块:
- 证书管理:了解 P12 证书解析与 OTA 安全常量获取,参见 证书管理:P12 解析、OTA 证书与 ECU 级安全常量获取
- SeedToKey 与 CRC16:诊断安全访问中的密钥计算与校验,参见 SeedToKey 与 CRC16:诊断安全访问的密钥计算
- VBF 文件解析:理解加密刷写文件的完整解析流程,参见 VBF 文件解析器:VBFHeader、VBFBlock 与 RSA 签名验证
- JiduClient 通信:了解 ECDSA 签名在 HTTPS 双向认证中的实际应用,参见 JiduClient:与集度服务器的 HTTPS 双向认证通信客户端