跳转至

云端音频格式说明

关于本文编码参数分析的重要说明:客户端 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 包携带多长时间的音频)决定,计算公式:

每秒发包数 = 1000 / ptime(ms)

语音通信行业最常用的 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;
}

原理拆解

  1. XOR 是自逆运算,加密解密用同一个函数A XOR B XOR B = A。对原始数据调用一次即完成"加密";把加密结果再调用一次同一函数(同一密钥),即可"解密"还原——不存在专门的加密函数和解密函数,二者是同一段代码。
  2. 运算对象是字节,不是字符串本身:数据 buffer 的每个字节,与密钥字符串对应位置字符的 ASCII 码值做按位异或。
  3. 密钥循环重复使用:因为密钥长度远小于待处理数据长度,用 i % keyLen 让密钥循环滚动使用。例如密钥为两个字符 [K0, K1],数据为四个字节 [D0, D1, D2, D3]
结果[0] = D0 XOR K0
结果[1] = D1 XOR K1
结果[2] = D2 XOR K0   ← 密钥用完后循环回第0位
结果[3] = D3 XOR K1

这种"密钥循环重复对数据逐字节异或"的方式,密码学上称为重复密钥 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