云端音频格式说明¶
关于本文编码参数分析的重要说明:客户端 SDK 中实现了编码器(Opus/Speex)的编码函数,但在当前 TTS 播放的实际业务流程中,客户端只调用了这两个编码库的解码路径——云端下发的音频数据在服务器侧已完成编码,客户端仅负责解码播放。因此,本文中涉及编码器内部配置(帧长、应用模式、复杂度、码率等)的分析基于客户端 SDK 代码中的编码函数实现,反映的是该函数被调用时会使用的配置,但这条编码路径在当前业务流程下并未被实际触发;云端服务器侧真实生效的编码配置无法从本仓库代码确认。
1. 语音识别(SR):上传至云端的录音格式¶
上传给云端识别引擎的录音格式固定为:
- 编码:PCM
- 采样精度:16bit(S16-LE)
- 采样率:16000Hz
- 声道:单声道
该格式为固定要求,不提供其他编码格式可选。
2. 语音合成(TTS):云端下发的音频编码格式¶
云端语音合成结果支持以下编码格式:
| 格式 | 说明 |
|---|---|
| raw | 未压缩 PCM/WAV |
| speex | Speex 窄带编码 |
| speex-wb | Speex 宽带编码 |
| opus | Opus 窄带编码 |
| opus-wb | Opus 宽带编码(默认) |
| opus-swb | Opus 超宽带编码 |
暂不支持 MP3 格式。
窄带 / 宽带 / 超宽带的含义¶
"窄带(NB)""宽带(WB)""超宽带(SWB)"描述的是编码器覆盖的音频频率带宽,对应输入/输出的采样率,与位宽(采样精度)、传输速率(码率)是不同维度的概念:
| 带宽等级 | 频率范围 | 对应采样率 |
|---|---|---|
| 窄带 NB | ~300–3400Hz(电话音质) | 8kHz |
| 宽带 WB | ~50–7000Hz | 16kHz |
| 超宽带 SWB | ~50–14000Hz | 32kHz |
带宽等级越高,能保留的高频语音细节越多(如齿音、气声),听感更清晰自然;同等音质下所需码率通常也随之升高,但码率和带宽等级并非同一概念,同一带宽等级下也可以用不同码率编码。
3. 语音编码技术分类:波形编码 / 参数编码 / 混合编码¶
语音编码技术按原理可分为三大类,Speex/Opus 采用的正是其中的混合编码技术:
波形编码(Waveform Coding)¶
直接对语音信号的波形本身进行量化编码,尽可能精确地还原原始波形,不依赖语音产生过程的模型假设。
- 代表技术:PCM、ADPCM
- 优点:中高码率下音质好、还原度高
- 缺点:压缩率有限,码率降到很低时音质会明显劣化
- 本方案中的
raw(未压缩 PCM)即属于波形编码的直接体现
参数编码(Parametric Coding)¶
不编码波形本身,而是提取语音产生过程的模型参数(如声道谐振特征、基音周期、音量增益等),解码端依据这些参数重新合成语音,也称声码器(Vocoder)编码。
- 代表技术:早期 LPC 声码器
- 优点:码率可以压得非常低(几 kbps 量级)
- 缺点:合成语音自然度较低,容易带有"机械感",因为是参数化重建而非波形重建
混合编码(Hybrid Coding)¶
结合波形编码与参数编码的优点:先用参数化模型(线性预测)去除语音信号中可预测的成分,再对剩余的残差部分采用类似波形编码的码本匹配方式编码,兼顾中低码率下的压缩率与音质。
- 代表技术:CELP(码激励线性预测)及其各类变体,是目前主流语音通信编码标准(如 AMR、G.729)的技术基础
- Speex 基于 CELP 框架,属于混合编码
- Opus 内部融合了 SILK(基于 LPC 的混合编码技术,擅长语音)与 CELT(基于变换域编码、更偏向波形/频域编码,擅长音乐)两种技术,可根据内容与码率自适应选择或混合使用,因此 Opus 本身也被称为"混合编码器"
这也是 Speex/Opus 相比直接采用波形编码(如 WAV/PCM)能在低码率下仍保持较好语音可懂度和自然度的技术原因。
4. 涉及的开源组件¶
| 组件 | 版本 | 许可证 | 来源 |
|---|---|---|---|
| Speex | 1.2.0 | BSD | https://www.speex.org |
| Opus | 1.4 | BSD-3-Clause-Clear | https://opus-codec.org |
说明:Speex 官方项目包含 libspeex(编解码器)与 speexdsp(预处理库,含降噪/AGC/回声消除/VAD 等能力)两部分,本文所述仅使用其 libspeex 编解码能力,用于音频压缩传输;降噪等预处理能力不在本方案使用范围内。
Opus 的划分方式与 Speex 不同:Opus 本身不含降噪等 DSP 预处理能力,只是纯编解码器;官方生态中另有 libopusenc 用于将 Opus 帧封装为标准 .opus/Ogg 容器文件。本方案仅使用 Opus 的裸编解码能力生成编码帧用于流式网络传输,不涉及容器文件封装。
5. 为什么不采用 WAV / MP3 / WMA¶
- WAV 为无压缩格式,同等音质下码率显著高于压缩编码,在网络实时下发场景下会带来更高的带宽占用和首包延迟,不适合流式播放场景。
- MP3 主要面向存储/离线播放场景设计,编解码延迟未针对实时流式通信优化;同时历史上存在专利授权相关的商用许可成本考量。
- WMA 是微软的私有格式,跨平台(尤其是 Android/Linux/QNX 等嵌入式与移动端)支持薄弱,编解码库生态主要围绕 Windows,集成到非 Windows 平台成本高;同样面向通用音乐/音频场景设计,不是针对实时语音通信优化的编码器。
- Speex / Opus 均为面向语音通信场景设计的编解码器,压缩率高、延迟低、抗丢包能力强,且许可证均为 BSD 系,无专利授权负担,跨平台支持成熟,更适合云端流式合成、边生成边播放的场景。Opus 覆盖窄带到超宽带全场景,因此作为默认编码格式。
6. 编码软延时(算法延时)情况¶
编码算法本身固有的处理延时(不含网络传输、抖动缓冲、解码等环节),由两部分构成:缓冲延时(编码器需要凑够一帧数据才能开始处理,等于帧长)+ 前瞻延时(lookahead,算法为预测分析需要提前读取一小段未来采样点)。
本方案所有编码路径(speex/speex-wb、opus/opus-wb/opus-swb)均使用 20ms 帧长;Opus 使用面向语音通信设计的应用模式(会启用 SILK/混合模式以获得更好的语音质量)。对应的典型算法延时(业界公开数据):
| 编码器 | 典型算法延时 |
|---|---|
| Speex | 约 30–34ms |
| Opus(20ms 帧长) | 约 22.5ms |
| MP3(对比参考) | 100ms 以上 |
| 未压缩 PCM/WAV(对比参考) | 趋近于 0 |
即便帧长相同,Opus 的算法延时也明显低于 Speex,两者都远低于 MP3,这也是选择二者做实时语音流式传输编码的原因之一。
7. 位宽(采样精度)为什么在压缩编码中很少提及¶
- PCM/WAV 等无压缩格式按采样点直接存储,每个采样点用固定 bit 数表示幅度(如 16bit),位宽直接决定文件大小和量化精度,是有意义的可配置参数。
- Speex/Opus 等压缩编码不直接存储采样点,而是提取语音特征参数(如频谱系数、基音周期、增益等)做量化和熵编码,量化精度由编码器按码率目标动态分配,并非固定的"每采样点多少 bit",因此无法用一个位宽数值描述。
- 压缩格式真正对外暴露、可配置的是码率(bitrate):给定采样率(带宽等级)后,设置目标码率,编码器自适应分配比特预算。解码后内部会重建出 PCM 信号(通常 16bit 或 float),但这只是解码器内部实现细节,不是压缩格式的可配置参数。
- 因此,位宽是无压缩 PCM 的量化精度描述,压缩编码用码率替代了这个角色,两者不在同一套参数体系里。
8. 完整性校验情况¶
Speex/Opus 编解码器码流本身不带 CRC/checksum 等完整性校验机制,这类校验通常由容器格式(如 Ogg 的 CRC32 页校验)或网络传输协议(如 RTP 的序号机制、TCP 校验和)承担。
RTP(Real-time Transport Protocol,实时传输协议)与 Speex/Opus 是不同层面的概念:RTP 是负责实时媒体网络传输的协议,本身不做音频压缩,只负责把编码器产生的数据打包传输;业界有标准的 Speex-over-RTP 封装规范(RFC 5574),RTP 包头自带的序列号和时间戳可以用于丢包检测、乱序处理和播放同步,是 VoIP/实时语音通话场景的常见组合。RTP 依赖 UDP(少数场景用 TCP)作为底层承载,严格分层上位于 UDP 之上,而非与 TCP/UDP 同属传输层。
本方案未采用 RTP 传输,云端音频数据是通过自定义协议(Base64 编码嵌入 JSON)下发的,不涉及 RTP 或 Ogg 容器封装,因此不具备传输层/容器层原生的完整性校验与序号连续性能力。本方案的音频数据完整性依赖以下常规处理:
- 云端下发数据先做 Base64 格式校验,格式不合法则丢弃该帧
- 解码后核对输出音频长度是否与期望帧长一致
- 依赖编解码库自身的返回值判断解码是否成功
- 一旦某一帧解码失败,当前策略是终止本次播放,不做跳帧重试或丢帧补偿
9. RTP 传输音频的典型码率(背景知识)¶
RTP 本身不规定码率,实际传输速率由所承载的音频编解码器决定,此外还需叠加 RTP/UDP/IP 包头开销(约 40 字节/包,IPv4 场景)。常见语音编解码器的典型负载码率:
| 编解码器 | 典型码率 |
|---|---|
| G.711(PCM,电话网常用) | 64 kbps |
| G.729 | 8 kbps |
| G.722(宽带) | 64 kbps |
| Speex | 约 2.15–24.6 kbps(窄带),最高 34.2 kbps(宽带) |
| Opus | 6–510 kbps,语音场景常用 16–32 kbps |
低码率编码(如 G.729)在加上包头开销后,实际网络占用可能接近负载本身的 2 倍;Opus/Speex 码率本身较高,开销占比相对较小。
需要说明的是:本方案未使用 RTP 传输,且如上文所述,TTS 云端音频的实际编码环节发生在服务器侧、不在本仓库代码范围内,因此上表仅作行业背景参考,不代表本方案云端下发音频的实际码率。
10. RTP 发包频率与封装结构(背景知识)¶
发包频率¶
RTP 发包频率不是固定值,由打包时长(ptime,每个 RTP 包携带多长时间的音频)决定,计算公式:
语音通信行业最常用的 ptime 是 20ms,对应每秒 50 个包;也有系统用 10ms(每秒 100 包,延时更低但包头开销占比更高)或 30ms(每秒约 33 包,开销更低但延时更高)。像"一秒 10 帧"这种打包间隔(对应 100ms/包)在实时语音通信场景里很少见,因为单包携带的音频时长过长,会显著增加端到端延时,通常只出现在对实时性要求很低的场景。
封装结构¶
音频从编码帧到网络传输,是逐层封装的关系:编码后的音频帧作为 RTP 的负载(Payload),RTP 包整体再作为 UDP 的负载,UDP 数据报再作为 IP 数据包的负载:
┌───────────────────────────────────────────────────────────┐
│ IP 头部 (IPv4 20字节) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ UDP 头部 (8字节) │ │
│ │ ┌─────────────────────────────────────────────────────┐│ │
│ │ │ RTP 头部 (12字节起) ││ │
│ │ │ ┌───────────────────────────────────────────────────┤│ │
│ │ │ │ RTP 负载:编码后的一帧音频数据(如一帧 Opus/Speex 帧) ││ │
│ │ │ └───────────────────────────────────────────────────┘│ │
│ │ └─────────────────────────────────────────────────────┘│ │
│ └───────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
IP 头部关键字段(IPv4):
| 字段 | 含义 |
|---|---|
| 版本 | IP 协议版本(4 或 6) |
| 总长度 | 整个 IP 数据包的字节数(含头部+负载) |
| 标识 / 标志 / 片偏移 | 用于 IP 分片与重组 |
| 生存时间(TTL) | 数据包可经过的最大路由跳数,防止无限转发 |
| 协议号 | 标识上层协议,UDP 对应 17 |
| 头部校验和 | 仅校验 IP 头部本身是否损坏 |
| 源/目的 IP 地址 | 发送方与接收方的网络地址 |
UDP 头部字段:
| 字段 | 含义 |
|---|---|
| 源端口 / 目的端口 | 标识发送方/接收方的应用进程 |
| 长度 | UDP 头部 + 负载的总字节数 |
| 校验和 | 校验 UDP 头部+负载数据是否损坏(可选,IPv4 下可为0表示不校验) |
RTP 头部字段:
| 字段 | 含义 |
|---|---|
| V(版本,2 位) | RTP 协议版本,当前为 2 |
| P(填充标志,1 位) | 负载末尾是否有填充字节 |
| X(扩展标志,1 位) | 是否携带扩展头 |
| CC(CSRC 计数,4 位) | 特约信源(CSRC)数量,多用于混音场景 |
| M(标记位,1 位) | 标识特殊事件,如一段话音的起始帧 |
| PT(负载类型,7 位) | 标识负载使用的编码格式(如 PCMU、Opus 等,决定接收端用哪个解码器解析) |
| 序列号(16 位) | 每发一个包递增 1,接收端据此检测丢包、乱序 |
| 时间戳(32 位) | 反映该帧音频的采样时刻,按采样率步进(不是发送时刻),用于播放端同步和抖动缓冲 |
| SSRC(32 位) | 同步信源标识,唯一标识一路媒体流的发送端 |
也可以看出,前面提到的"完整性校验"能力(序列号连续性检测),正是由 RTP 头部的序列号字段提供的——这也是本方案因为没有使用 RTP、改走自定义协议,所以缺失这层能力的具体原因。
一个包里能否装多帧:帧聚合(frame aggregation)¶
Speex 的 RTP 载荷格式(RFC 5574)支持把多个 Speex 帧打包进一个 RTP 包,这是常见的开销优化手段:
- Speex 单帧压缩后体积很小(几十字节量级),若一帧一个 RTP 包,RTP+UDP+IP 头部开销(约 40 字节/包)占比会很高,甚至超过负载本身
- Speex 码流支持字节对齐拼接:多帧编码后各自补齐 padding bit 到字节边界后首尾相连,解码端连续读帧直到数据耗尽或遇到终止码(5 个连续 '1' 比特)
- 一个包聚合几帧(即实际打包时长 ptime)是会话建立阶段通过 SDP 协商声明的(如
a=ptime:40表示每包聚合 2 个 20ms 帧),不依赖 RTP 头本身的字段 - 权衡:聚合帧数越多,头部开销占比越低,但端到端延时越高,且单包丢失造成的连续音频缺失也越多
Opus 更进一步,其原生码流格式(TOC 字节的 code 2/3 机制)自身就支持一个"包"内携带多帧,属于编码器层面的能力,不完全依赖 RTP 载荷格式;而 Speex 的多帧聚合纯粹是 RTP 载荷格式层面的做法,裸 Speex 码流本身没有"一包多帧"的内建标记。
RTP 本身没有加密能力,及行业合规做法¶
RTP(RFC 3550)设计上只负责打包、排序、加时间戳,负载是明文传输,其底层依赖的 UDP 也不提供加密,只有一个弱校验和。在涉及用户隐私数据(尤其车载场景下的语音指令、车内对话)的产品中,明文传输语音数据存在合规风险。
行业标准的加密方案:
- SRTP(Secure RTP,RFC 3711):RTP 的加密扩展,用 AES 加密负载、HMAC-SHA1 做完整性认证与防重放,是 VoIP/实时通信行业加密语音传输的标准方案;SRTP 本身只负责加密和认证,不负责密钥协商
- DTLS-SRTP(RFC 5764):WebRTC 采用的标准做法,通过 DTLS 握手协商 SRTP 会话密钥,实现端到端加密
- SDES:密钥直接放在 SDP 信令中传输,依赖信令通道本身已加密,实现简单但安全性弱于 DTLS-SRTP,常见于传统 VoIP 系统
- 不走 RTP,直接用已加密的应用层通道传输:即把音频数据封装进 HTTPS/WSS(WebSocket over TLS)等已加密通道,不需要再单独给 RTP 层加密。本方案的 TTS 云端交互(音频数据 Base64 编码嵌入 JSON)即属于此类思路,实际加密效果取决于底层连接是否使用 TLS,具体见下一节。
12. 字符串加密策略¶
网络传输层面采用 TLS 协议加密。本地数据/字符串的加密策略主要分两类:轻量的重复密钥 XOR 混淆,和标准的 AES 对称加密,按数据敏感程度选用。
12.1 重复密钥 XOR(repeating-key XOR)¶
zhihu:参考链接 核心逻辑:
int Xor(void* pStr, int nSize, const char* key)
{
char* pStrTmp = (char*)pStr;
unsigned int keyLen = strlen(key);
for (unsigned int i = 0; i < (unsigned int)nSize; i++) {
pStrTmp[i] = pStrTmp[i] ^ key[i % keyLen];
}
return 0;
}
原理拆解:
- XOR 是自逆运算,加密解密用同一个函数:
A XOR B XOR B = A。对原始数据调用一次即完成"加密";把加密结果再调用一次同一函数(同一密钥),即可"解密"还原——不存在专门的加密函数和解密函数,二者是同一段代码。 - 运算对象是字节,不是字符串本身:数据 buffer 的每个字节,与密钥字符串对应位置字符的 ASCII 码值做按位异或。
- 密钥循环重复使用:因为密钥长度远小于待处理数据长度,用
i % keyLen让密钥循环滚动使用。例如密钥为两个字符[K0, K1],数据为四个字节[D0, D1, D2, D3]:
这种"密钥循环重复对数据逐字节异或"的方式,密码学上称为重复密钥 XOR,思路上与维吉尼亚密码(Vigenère cipher)一致,只是把模加法换成了 XOR。 4. 安全强度评估:密钥固定且长度较短时,若密文样本足够多,可以用频率分析等手段推出密钥(类似破解维吉尼亚密码的方法),因此这类方案只能算"轻量混淆",不能作为保护机密数据的正式加密手段。
12.2 AES 对称加密¶
部分敏感数据使用标准 AES 对称加密,密钥管理分两种方式:
- 固定密钥:密钥以常量字符串形式内置于程序中,加解密双方使用相同密钥,实现简单但密钥一旦泄露则整体失效。
- 设备绑定派生密钥:基于设备硬件特征信息做哈希运算生成密钥,不同设备的密钥不同,即使程序被提取,密钥也无法直接跨设备复用。
加密模式方面存在两种实现:一种使用 ECB 模式(不需要 IV,每个分组独立加密);另一种使用 CBC 模式(分组之间存在链式依赖,需要 IV)。ECB 模式的已知弱点是:相同的明文分组会产生相同的密文分组,如果数据中存在重复内容,密文会暴露这种重复规律,安全性弱于 CBC。其中一处 CBC 实现的初始向量(IV)由普通伪随机数发生器生成,并非密码学安全随机数生成器(CSPRNG),可预测的 IV 会进一步削弱 CBC 本应提供的保护,属于实现层面的缺陷,建议后续改用 CSPRNG,并统一改用 CBC 或更安全的模式(如 GCM)。
PKCS7 填充原理:AES 是分组密码,每个分组固定 16 字节,原文长度不是 16 的整数倍时需要填充。PKCS7 的规则是"补的字节数量,就是补的每个字节的数值"——比如还差 8 字节凑满一个分组,就补 8 个值为 0x08 的字节;解密时只需读取密文解出来的最后一个字节的数值 N,即可知道去掉末尾 N 个字节还原原文。特别地,如果原文长度正好是 16 的整数倍,仍会额外补一整个分组(16 个 0x10),避免"最后一块是否为纯填充"的歧义。
C++ demo(使用开源 OpenSSL 库的 EVP 接口,已编译运行验证):
// g++ -std=c++14 aes_demo.cpp -lcrypto -o aes_demo
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <vector>
#include <stdexcept>
std::vector<unsigned char> pkcs7Pad(const std::vector<unsigned char>& data, int blockSize) {
int padLen = blockSize - static_cast<int>(data.size() % blockSize);
std::vector<unsigned char> out = data;
out.insert(out.end(), padLen, static_cast<unsigned char>(padLen));
return out;
}
std::vector<unsigned char> pkcs7Unpad(const std::vector<unsigned char>& data) {
unsigned char padLen = data.back(); // 最后一个字节的数值 = 填充长度
for (int i = 0; i < padLen; ++i) {
if (data[data.size() - 1 - i] != padLen) throw std::runtime_error("padding 校验失败");
}
return std::vector<unsigned char>(data.begin(), data.end() - padLen);
}
std::vector<unsigned char> aesEcbEncrypt(const unsigned char* key,
const std::vector<unsigned char>& plainPadded) {
std::vector<unsigned char> out(plainPadded.size() + 16, 0);
int outLen1 = 0, outLen2 = 0;
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
EVP_EncryptInit_ex(ctx, EVP_aes_128_ecb(), nullptr, key, nullptr);
EVP_CIPHER_CTX_set_padding(ctx, 0); // 关掉库自带padding,用自己实现的pad
EVP_EncryptUpdate(ctx, out.data(), &outLen1, plainPadded.data(), (int)plainPadded.size());
EVP_EncryptFinal_ex(ctx, out.data() + outLen1, &outLen2);
EVP_CIPHER_CTX_free(ctx);
out.resize(outLen1 + outLen2);
return out;
}
// aesEcbDecrypt / aesCbcEncrypt 结构类似,分别调用 EVP_Decrypt* 和 EVP_aes_128_cbc()
实际运行结果验证:24 字节原文补齐到 32 字节、末字节值为 8、解密精确还原;ECB 模式下两个相同的 16 字节明文块加密出完全相同的密文块,CBC 模式下同样两块的密文却不同——与前述原理一致。
12.3 关于本地 AES 实现的开源组件说明¶
网络传输层的 TLS 依赖有明确的开源组件登记(名称、版本、许可证,见第 4 节)。而本地文件/字符串加密所用的 AES 实现,并非调用某个具有明确名称和许可证登记的知名开源库,其来源与授权情况在现有代码中缺乏明确记录,建议后续补充确认代码来源与许可证信息,纳入开源软件合规清单管理。
12.4 密钥应该如何选取(通用最佳实践)¶
- 使用密码学安全随机数生成器(CSPRNG)生成密钥,而不是人工挑选的、有意义的字符串或单词组合——人工挑选的字符串往往遵循某种命名规律,可预测性远高于真随机字节序列。
- 避免在客户端分发的程序中硬编码静态密钥:如果所有分发实例共用同一个固定密钥,一旦任意一份被逆向分析出密钥,所有实例的防护同时失效。更稳妥的做法包括:密钥与设备/账户绑定动态派生、密钥由服务端在鉴权后动态签发、或采用密钥管理系统(KMS)/硬件安全模块(HSM)托管。
- 支持密钥轮换:密钥应可版本化更新,而不是编译进二进制后就无法更换;常见做法是"密钥分层"(envelope encryption)——用一个可轮换的密钥去加密实际的数据密钥,轮换上层密钥时无需重新加密全部数据。
- 区分"轻量混淆"与"真正的加密":如果目标只是防止普通用户直接查看内容(如本地日志),可以接受较弱的混淆方案,但应在设计上明确标注这只是混淆,不能用于保护身份信息、生物特征等真正敏感的数据;后者应始终采用标准加密算法配合合理的密钥管理。
12.5 未加密场景¶
部分本地缓存文件不做加密,仅通过 MD5 校验文件完整性(用于检测文件损坏/篡改,不提供机密性保护)。
12.6 常见的 C++ 开源加密组件¶
| 组件 | 特点 | 许可证 |
|---|---|---|
| OpenSSL | 最广泛使用的密码学库,涵盖对称/非对称加密、哈希、TLS 等全套能力 | Apache 2.0 |
| mbedTLS | 面向嵌入式场景的轻量级 TLS/加密库,本方案网络传输层已使用(见第 12.1 节前的说明) | Apache 2.0 |
| Crypto++ | 老牌 C++ 密码学库,算法覆盖面广 | Boost 类许可 |
| libsodium | 基于 NaCl,接口设计强调"难以误用",API 现代简洁 | ISC |
| Botan | 功能完善的 C++ 密码学库 | BSD-2-Clause |
| wolfSSL | 面向嵌入式/IoT 场景的轻量 TLS/加密库 | GPLv2 / 商业双授权 |
12.7 其他加密技术简介¶
除了本文重点介绍的重复密钥 XOR 与 AES 对称加密外,密码学中还有以下几类常见的加密/认证类技术,此处仅作概念性介绍,不展开实现细节(哈希函数单独成章,见第 13 章):
- 非对称加密(公钥加密):如 RSA、ECC(椭圆曲线)。用一对数学上相关但不同的密钥——公钥加密、私钥解密(或反过来用私钥签名、公钥验证)。公钥可以公开分发,私钥必须保密。由于运算开销远高于对称加密,通常不直接用来加密大量数据,而是用于密钥交换、身份认证、数字签名等场景。
- 消息认证码(MAC/HMAC):在哈希基础上引入密钥,既能验证数据完整性,也能验证数据来源的真实性(防篡改+防伪造),比单纯哈希多了"身份认证"的能力。
- 数字签名:基于非对称加密,用私钥对数据(通常是数据的哈希值)签名,任何拿到公钥的人都能验证签名是否有效,用于身份认证和防抵赖(签名方无法事后否认自己签过)。
- 密钥交换协议:如 Diffie-Hellman(DH)、ECDH。让通信双方在不安全的网络上,不需要提前共享密钥,就能协商出一个只有双方知道的共享密钥,常作为建立 TLS 连接等场景的前置步骤。
13. 哈希函数¶
13.1 哈希函数与加密的本质区别¶
哈希函数和加密解决的是不同的问题,不宜理解为"哈希是加密的基础",更准确的说法是二者是密码学工具箱中平级的基础构件:
- 加密是可逆的:明文 → 密文,持有正确密钥的一方能够还原出明文,信息量不丢失,只是被密钥"锁住"。
- 哈希是不可逆的(且是故意设计成不可逆):任意长度数据 → 固定长度"指纹",目的不是保密,而是生成一个简短、确定、有代表性的标识,用于完整性校验、比对、索引;哪怕输入只改动一个字节,输出也会产生完全不同的结果(雪崩效应)。
哈希函数不是加密算法的底层构造材料(例如 AES 有自己独立的内部结构,并非用哈希函数搭建)。
13.2 哈希碰撞风险详解¶
哈希函数把任意长度的输入映射为固定长度的输出,输入空间无穷大而输出空间有限,根据抽屉原理,不同输入映射到相同输出(即"碰撞")在数学上必然存在,这不是漏洞,是哈希函数的固有属性。
真正的安全要求不是"没有碰撞"(不可能做到),而是抗碰撞性:即使碰撞存在,也没有人能在现实时间内故意构造出一对碰撞。
- SHA-256:目前没有找到比暴力穷举更快的碰撞构造方法,穷举计算量是天文数字,认为是安全的。
- MD5:已被找到实际可行、几分钟内可在普通电脑上完成的碰撞构造方法,不能再用于任何安全场景。
危害场景(碰撞攻击的前提是"攻击者能主动构造恶意输入"):
- 完整性校验被绕过:攻击者构造出内容被篡改、但哈希值与原始文件相同的恶意文件,校验会误判为"未被篡改"。
- 数字签名伪造:数字签名通常只对文档的哈希值签名而非全文。若哈希函数不抗碰撞,攻击者可以准备好两份哈希值相同但内容不同的文档,拿到对其中一份的合法签名后,声称该签名同样适用于另一份(历史上真实发生过的证书伪造攻击手法)。
需要澄清的概念区分:碰撞风险(能否故意构造出两个不同输入、哈希值相同)不等于哈希可以被反算出原文(那是另一个独立性质,称为"抗原像性")。
MD5/SHA-1 现在的现实意义:碰撞漏洞只在"存在主动构造恶意输入的攻击者"场景下才有威慑力,在纯粹检测意外损坏(无攻击者、无对抗动机)的场景下,随机产生碰撞的概率依然极低,MD5/SHA-1 依然可以合理使用,例如:
- 文件传输/存储的意外损坏检测(无攻击者主动构造恶意内容的场景)
- 数据去重、哈希表、缓存 key 等索引场景(不涉及"防伪造")
- 遗留系统兼容(如老旧协议内部仍以其作为内容标识,而非防伪造凭证)
必须避免使用 MD5/SHA-1 的场景:数字签名、证书签名、任何攻击者可控制输入内容的完整性校验、密码存储(密码存储的问题主要不是碰撞,而是这类通用哈希算得太快、易被暴力穷举/彩虹表破解,应使用专门的密码哈希算法)。
13.3 常见哈希函数安全现状¶
| 哈希函数 | 输出长度 | 安全现状 |
|---|---|---|
| MD5 | 128 位 | 已被攻破,可实际构造碰撞,不建议用于安全场景 |
| SHA-1 | 160 位 | 已被攻破,2017 年已有公开的实际碰撞攻击案例,主流软件已停止信任其签名 |
| SHA-2 系列(SHA-256/384/512 等) | 256/384/512 位 | 目前公认安全,是当下最主流的选择 |
| SHA-3(Keccak) | 224/256/384/512 位 | 目前公认安全,内部结构(海绵结构)与 SHA-2 不同,作为备用方案而设计 |
| BLAKE2 / BLAKE3 | 可变 | 目前公认安全,非官方标准但业界广泛使用,计算速度更快 |
13.4 哈希函数计算速度对比(实测数据)¶
不同哈希算法的计算速度存在明显差异,以下是在一台 x86_64 开发机上使用 OpenSSL 基准测试工具实测的数据(8192 字节数据块吞吐量,以 MD5 为基准值 1):
| 哈希算法 | 吞吐量(KB/s) | 相对 MD5 速度 |
|---|---|---|
| MD5 | 637,110 | 1.00(基准) |
| SHA-1 | 774,052 | 1.22 |
| SHA-256 | 289,849 | 0.46 |
| SHA-512 | 475,662 | 0.75 |
| SHA3-256 | 286,591 | 0.45 |
| BLAKE2b | 629,678 | 0.99 |
关键说明:
- 该结果依赖测试机 CPU 是否具备对应哈希算法的硬件加速指令。本次实测环境的 CPU 支持 SHA-1/SHA-256 的硬件加速指令,但 MD5 从未获得过硬件加速支持,因此实测中 SHA-1 反而比 MD5 更快,与"MD5 最快"的传统印象相反。
- SHA-256/SHA3-256 明显慢于 MD5/SHA-1/BLAKE2(约慢一倍),因为内部运算更复杂、轮数更多,这是"更高安全性通常意味着更大计算开销"的直接体现。
- BLAKE2 的设计目标是"接近 MD5 的速度、接近或超过 SHA-2 的安全性",实测结果与设计目标一致。
- 该比值高度依赖具体硬件:若目标平台的 CPU 不具备对应的加密硬件加速指令(例如部分嵌入式/移动端芯片),实测关系可能回到传统认知——MD5/SHA-1(运算更简单、轮数更少)比 SHA-256/SHA-3 更快。若需要准确评估特定目标硬件上的性能表现,应在该硬件上重新实测,不能直接套用开发机的测试结果。
13.5 哈希函数在密码学协议中的组合应用¶
哈希函数虽不是加密算法的构造材料,但在实际安全协议中经常与加密组合使用,承担支撑性角色:
- HMAC:哈希函数 + 密钥的直接组合,兼具完整性校验与来源认证能力。
- 数字签名:先对数据算哈希得到短"指纹",再用私钥对哈希值签名(而非对全文签名,以降低运算量),哈希在此是签名流程的前置步骤。
- 密钥派生(KDF):如 PBKDF2、HKDF,通常基于哈希函数(或 HMAC)反复迭代运算生成加密密钥,此场景下哈希确实是"生成密钥"这一步的基础组件。
- 密码存储:应使用专门设计的密码哈希算法(如 bcrypt、scrypt、Argon2),利用的正是哈希"不可逆"这一特性——只需验证密码是否正确,无需还原原始密码,用可逆的加密算法存储密码反而是设计错误。
14. 参考资料¶
- Speex 官方手册:https://www.speex.org/docs/manual/speex-manual.pdf