13. DRM与安全媒体路径:Widevine DRM集成、安全解码器、加密与解密流程

说到DRM,很多做上层应用的朋友可能觉得离自己很远。但如果你做过视频类App,尤其是那些需要播放付费内容的——比如Netflix、腾讯视频、爱奇艺——你一定会碰到它。

DRM全称是Digital Rights Management,数字版权管理。说白了,就是防止你偷偷把视频录下来、截屏、或者把文件拷走。Android系统里最主流的DRM方案就是Widevine,Google自家的东西。我这些年做多媒体框架,跟Widevine打交道最多,踩过的坑也不少。

13.1 Widevine DRM 是什么?

Widevine是Google收购来的一个DRM方案。它分三个安全等级:

等级 安全级别 说明
L3 最低 纯软件解密,密钥在内存中,容易被dump
L2 中等 解密在硬件中,但解码在软件中
L1 最高 全硬件安全路径,从解密到显示都在TrustZone里

我个人习惯把L1叫做“真正的安全”。L3说白了就是个摆设,防君子不防小人。我在项目里遇到过客户要求L1,结果发现他们的SoC根本不支持——嗯,那就要跟产品经理好好聊聊了。

13.2 安全媒体路径的整体架构

先看一张图,理解一下整个安全媒体路径是怎么走的:

Widevine DRM 安全媒体路径 加密媒体内容 (HLS/DASH 流) MediaDrm (密钥请求/响应) 安全解码器 (OMX/Codec2) 显示 解密与解码流程 加密数据 (AES-128) 硬件解密 (TrustZone) 硬件解码 (H.264/H.265) 安全渲染 (SurfaceFlinger) 安全边界 (Secure Boundary) 密钥管理流程 1. App 通过 MediaDrm 发起密钥请求 (Key Request) 2. License Server 返回密钥响应 (Key Response) 3. 密钥存储在 TrustZone 安全区域,用户空间不可见

这张图我画了好几次才满意。你注意看那个“安全边界”的虚线框——框里面的东西,普通应用层代码是碰不到的。这就是L1的核心思想。

13.3 MediaDrm 与 MediaCrypto

Android里跟DRM打交道的两个核心类:MediaDrmMediaCrypto

  • MediaDrm:负责密钥管理。跟License Server通信,获取解密密钥。
  • MediaCrypto:负责实际解密。它把密钥注入到解码器里。

我见过不少开发者搞混这两个东西。简单记:MediaDrm是“拿钥匙的人”,MediaCrypto是“用钥匙开门的人”。

核心流程:

  1. App创建MediaDrm实例,指定UUID(Widevine的UUID是edef8ba9-79d6-4ace-a3c8-27dcd51d21ed)
  2. 调用getKeyRequest()生成请求,发给License Server
  3. Server返回响应,调用provideKeyResponse()注入密钥
  4. 创建MediaCrypto,关联MediaDrm中的密钥
  5. 将MediaCrypto传给MediaCodec,开始解密播放

13.4 安全解码器的工作原理

安全解码器跟普通解码器有什么不同?我直接说结论:

普通解码器,解码后的YUV数据在共享内存里,App可以读。安全解码器,解码后的数据直接送到显示硬件,中间不走用户空间。你想想看,如果数据都不经过你的进程,你怎么录屏?

Android里安全解码器通过两个机制实现:

  • OMX IL(旧):组件名带 .secure 后缀,比如 OMX.qcom.video.decoder.avc.secure
  • Codec2(新):通过 C2Config::isSecure() 标记

小技巧: 调试时可以用 adb shell dumpsys media.player 查看当前使用的解码器是不是secure版本。我曾经靠这个帮客户定位了一个L1降级到L3的问题——他们用的解码器名字里没有.secure。

13.5 加密与解密流程详解

Widevine用的加密算法是AES-128,有两种模式:

模式 说明 使用场景
CBC Cipher Block Chaining,每个块依赖前一个块 HLS (TS 流)
CTR Counter Mode,可并行解密 DASH (FMP4 流)

解密流程大致是这样的:

// 伪代码,展示核心逻辑
// 1. 解析加密的媒体数据,提取出加密的sample
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int inputIndex = codec.dequeueInputBuffer(10000);
if (inputIndex >= 0) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputIndex);
    // 2. 填充加密数据到inputBuffer
    inputBuffer.put(encryptedData);
    // 3. 设置加密信息:IV、key ID、加密模式等
    MediaCodec.CryptoInfo cryptoInfo = new MediaCodec.CryptoInfo();
    cryptoInfo.set(numSubSamples, clearBytes, encryptedBytes, key, iv, mode);
    // 4. 提交给解码器,解码器内部调用硬件解密
    codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, pts, 0);
}

这里有个关键点:queueSecureInputBuffer 和普通的 queueInputBuffer 不同。前者会触发安全路径,数据在硬件内部解密。后者是明文路径。

注意: 千万不要在应用层手动解密再传给解码器!那样就绕过了安全路径,L1会降级成L3。我曾经遇到一个厂商的SDK就是这么干的,结果Netflix认证死活过不了。

13.6 实际项目中的避坑指南

做DRM集成,坑是真的多。我挑几个常见的说说:

  • License URL配置错误:Widevine的License Server地址必须跟你的DRM方案匹配。我见过有人把测试环境的URL带到生产环境,结果用户全看不了。
  • Key Box缺失:设备出厂前要预置Widevine的Key Box。有些小厂忘了这步,L1直接变L3。
  • HDCP回退:L1要求HDCP 2.2,如果显示器不支持,系统会回退到L3。这个逻辑在 SurfaceFlinger 里,调试起来很麻烦。
  • 多DRM方案共存:有些App同时支持Widevine和PlayReady,UUID要区分清楚,别搞混了。

嗯,说到HDCP回退,我记得有一次客户反馈说他们的设备在4K电视上只能播1080p。查了半天,发现是HDMI线不支持HDCP 2.2——硬件问题,软件再折腾也没用。

13.7 安全媒体路径的验证方法

怎么确认你的安全路径走对了?我一般用这几个方法:

  1. 看logcat:搜索 MediaCodecOMX 关键字,看解码器名字有没有 .secure
  2. dumpsys media.extractor:查看DRM信息,确认UUID和等级
  3. 抓取H264码流:如果安全路径正确,你抓到的码流应该是加密的,解不开
  4. 尝试录屏:L1下录屏应该是黑屏或者绿屏

一句话总结: Widevine DRM的核心就是“密钥在硬件里,解密在硬件里,显示也在硬件里”。只要数据不经过用户空间,安全就有保障。

做DRM集成,耐心很重要。很多时候不是代码写错了,而是环境配置不对。我建议你从L3开始调,通了再上L1。一步到位容易把自己搞崩溃。


公众号:蓝海资料掘金营,微信deep3321