如何在Android下实现DRM以及与其适配的Secure Video Path的要点

描述

DRM(Digital Rights Management)是一个成熟的操作系统中必须实现的功能。DRM提供的功能正如其字面的意思,可以帮助保护数字版权;目前最直接的一个应用就是对在线播放的媒体流进行保护。在Android下DRM相关的代码被放置在了多媒体的架构当中。

安卓的DRM架构目前常见的实现有两种。

经典的Android DRM Framework架构;

现在用的比较多的mediaDRM实现。

Android

DRM Framework架构图

Android

MediaDrm流程及其工作流程图

这两者的区别是DRM Framework考虑的是通用DRM实现;举例来说,当播放一个媒体源的时候,会有一些初始的与服务器交互得到的数据被DRM Manager所解析,来判断是否含有DRM信息;如果包含相关信息,则对应已注册的DRM Plugin会被选中用来处理DRM流程;并且在流程完毕以后负责媒体流的解密。

而mediaDRM则在简化了流程。mediaDRM的API设计主要是为了对接ISO/IEC 23001-7: Common Encryption(缩写CENC)标准;CENC定义了如何获得一个媒体流解密所需要的密钥的流程和数据格式。这个标准相对简洁,不过这个标准是收费的,笔者也没有能阅读详细的内容,只能从代码上略知一二。举例说明,当播放一个媒体流的时候,这个媒体流事先就定义好是哪种符合CENC标准的DRM场景(前面的DRM Framework中有一个嗅探的过程);对于此种DRM场景,Media Framework会直接去查找相应的mediaDRM插件来处理与服务器的交互,并且流程和信息都遵照CENC标准(DRM Framework中考虑的是通用实现,比如一种全新的DRM场景);最后得到密钥,来进行媒体流解密。

mediaDRM对于Player应用来说使用起来相对简单。很多常见的DRM实现基本使用这种方法。比如Widevine; Playready等。而且谷歌的开源播放器Exoplayer可以直接用来测试mediaDRM实现。

Android下实现了一个简单的开源mediaDRM 插件: ClearKey;读者可以通过研究这个插件而对mediaDRM的接口有所了解。ClearKey的路径在:frameworks/av/drm/mediadrm/plugins/clearkey/

由于需要比较好的实现DRM功能;并且现在的操作系统大多为开放式操作系统,被破解或者root的概率是相当的高;所以DRM对设备上从解密到播放的这一条通路都做了要求;要求媒体流数据从解密,被解码到显示的过程中一概不能被泄露;WidevineL1之类对此都有严格的要求。这种从解密到显示的通路称为Video Path;而保证安全的通路则称为Secure Video Path。

实现过程

对于通用的mediaDRM架构,比如上文提到的ClearKey;或者商用的DRM场景比如Widevine或者Playready;DRM交互协议部分基本已经实现,留下的与设备的密钥相关的操作一般需要被放置在一个安全的环境里进行。OEM一般需要阅读DRM场景的文档,配合DRM场景的要求实现OEM必须要实现的模块。实现这些模块是为了达到以下两个目的:

1. 将安全系统与DRM框架对接,以实现DRM框架所必须的安全功能;比如保护设备私钥等。常见的做法有使用硬件安全环境;或者运行在可信执行环境(TEE)的安全操作系统(Secure OS)。

保护密钥是最基本最重要的DRM要求。Widevine L2就是要求保护密钥;L1则是保护密钥+Secure Video Path;而L3基本只是为了测试Widevine协议而存在,既不保护密钥也不保护Video Path;

密钥的产生和维护过程,又是另一个安全相关的主题;在这篇文章里不做赘述。

2.实现一个安全通路使得从解密开始直到被显示都是安全的。

Android

为了达到这两个目的,以下组件需要进行必要的增加或者修改。

安全内存

要点:

实现安全内存分配器(比如ION Heap)

实现安全内存所需的配套设施(Secure Boot, TEE, Bootloader)

为了保存解密后的媒体流,为解码和显示做好准备,安全内存必须被提供。安全内存有许多实现方式。使用防火墙或者内存保护单元(MPU – Memory Protection Unit)是比较常见的方法。而对这些安全内存进行分配和使用的操作,Android提供了ION这个组件。

ION是一个安卓下统一的堆(Heap)管理接口。使用ION可以灵活的实现一些特定的内存管理器;正适合作为管理安全内存的接口。ION的实现基于DmaBuf;后者是一套内核API,可以实现在进程间的Dma内存共享;ION在内核API的基础上提供了接口供应用程序调用(/dev/ion);使得用户程序也能够分配在进程间共享的Dma内存。

最简单的安全内存实现则是在内存中预留一块区域为安全内存;使用MPU对此地址范围的内存进行保护,将不合格的存取请求拒绝。这一块预留的内存可以使用ION Heap管理起来;让用户程序可以在这个Heap里分配和释放内存;然而,仅仅是分配释放;想Memory Map以后再进行存取,是不可以的(MPU会拒绝非安全存取)。

MPU的规则只能在安全模式下定义;一般可以放在更早的启动组件里进行(Bootloader);如果具有动态内存权限设置功能的MPU,对MPU规则的设置可以放在Secure OS里完成。为了保证系统的完整性,安全启动(Secure Boot)必须被打开,验证Bootloader和Secure OS的完整性;防止非法篡改。

Linux中预留内存有多种方法。使用显式的内存预留是一种方法,参见dts代码:

reserved-memory {

#address-cells = <2>;

#size-cells = <2>;

ranges;

/* reserve memory for secure heap */

carveout: carveout@60000000 {

compatible = "ion,heap_secure";

reg = <0x0 0x60000000 0x0 0x02000000>;

};

}  

在上面的例子中,使用了carveout类型;carveout类型总体和安全内存的需求接近;但是Carveout Heap在分配的时候会负责清零;而非安全CPU访问内存是被MPU禁止的。所以需要一些改动,去除这些直接访问内存的地方。

经过以上一些列的设置,系统中的安全内存就被管理了起来。

目前常见的Android内核中,都为经典的ION接口API(alloc, free, map),这种方式有一个问题就是所有的Heap ID都是Hard Code。当用户在ION中添加了一个新Heap,则一个新的Heap ID需要被添加到ion.h中;然后复制到Android的bionic内核头文件的目录中;再运行脚本,将这个更新的头文件被复制到其他的lib头文件中(比如libion)。这样带来一些问题,一是因为在ion.h中,经典的代码把Heap Id和Heap Type给关联了起来;实际上这二者是独立的意义;二是Android使用repo管理很多的git仓库;假如使用前面修改ion.h的方法,一个简单的添加Heap Id的改动起码会影响三个左右的git仓库。所以在比较新的内核中ION添加了一个方法enumerate;使用这个方法可以得到当前所有的ION Heap的描述,根据描述得到目标Heap的ID,避免了频繁修改ion.h的问题。条件允许的话,建议大家尽量更新到后面的版本。

安全解密系统

要点:

实现在安全环境里解密并且将结果放入安全内存的操作

严格检查目标地址是否为安全地址

加密的媒体流是放在非安全内存里的。这部分的内容被解密以后结果会被放置到一个安全的环境里;同时这个解密的过程,也需要在一个安全的环境里。这里就涉及到安全解密系统。安全解密系统往往都是DRM实现的一部分。因为:

DRM流程中需要用到与设备有关的密钥来进行加解密行为。

解密媒体流所用的密钥最后也是在安全环境里被算出,并且解密过程需要在安全环境中进行。

目前通用的做法是将安全解密系统实现在安全操作系统中(Secure OS);在支持Arm Trustzone的芯片架构下,Secure OS可以访问系统的所有资源;在Secure OS中对加密的媒体流进行解密是比较适合的。另外还有其他类似的解决方案,比如硬件的安全加解密环境等。安全解密系统的职责就是解密,并且把数据放在安全内存中。这里比较重要的地方是,由于解密系统实际上是第一道检查安全内存的关卡,它有一个重要的责任就是,确认解密的目的地,必须是安全的。它需要检查目的地的范围和属性。

有一点需要说明的是,在Android中,解密系统是第一个处理媒体流的模块;但是它所使用的安全内存,是由视频解码器调用安全内存的接口(ION Heap)来分配的。

视频解码器

要点:

修改Codec组件函数enumerateComponents宣告支持Secure Codec类型

修改Codec组件函数makeComponentInstance支持创建Secure Codec实例

修改media_codecs.xml使得secure codec能够被Player枚举

修改内存分配函数,使得为Secure Codec实例分配安全内存成为可能

视频解码器需要支持安全解码;安全解码器能够存取安全内存。另一个重要的特点是,安全解码器,不能够存取普通内存。这是一个重要的原则,否则安全解码器就有可能将媒体流泄露到非安全内存中。在Android播放器一般的初始化流程中,初始化mediaCodec的时候,会为这个mediaCodec对象设置一个输出Surface:

codec.setOutputSurface(surface);

在上面一小节的介绍中,安全解密系统已经将解密后的媒体流放在了安全内存中等待解码。这个安全内存是由Codec组件分配,并且在调用解密函数的时候,传给安全解密系统的。这个存放待解码的媒体流的Buffer称为Input Buffer;在这里,由于需要使用安全内存,这里的Input Buffer是分配至安全内存的(通过调用ION接口);解码完成后放置帧数据的内存则来自Surface.

Android下为Secure Video Path所预留的设计是:当一个安全解码器被需要并且成功加载的时候,Android会激活整个Secure Video Path所需要的flag。安全解码器是否被需要,一般在mediaDrm Plugin的代码里会指定:

class CryptoPlugin : public android::CryptoPlugin{

...

virtual bool requiresSecureDecoderComponent(const char* mime) const {

/* TODO: check mime type */

return true;

}

...

}

如果DRM插件返回true的话,Player的一个职责就是需要初始化必要的安全解码器。安全解码器的名称,则是在普通的解码器名称后加上了一个后缀”.secure”。系统中所支持的解码器,都列在了media_codecs.xml中。下面的例子展示了如何添加一个安全解码器:

Android

其次在Codec的enumerateComponents中,需要在Media Framework中注册自己所支持的Codec类型。除了通常的decoder和encoder,decoder.secure是需要添加支持的。

Android

Player在根据所需要的解码器的mimeType,找到可用的Secure Codec以后,会去进行初始化。在初始化函数makeComponentInstance中,需要能够分配Secure Codec实例。一般来说,这个函数可以和普通的Codec的makeComponentInstance复用;只是发现Codec名称为”.secure”结尾的时候,在Codec Component内部的数据结构中置上一个Secure标志;以便后面分配内存的时候,能够知道当前的Codec Component是不是安全解码器:

Android

解码器组件在初始化实例的时候,需要提供实例所支持的接口给Media Framework,这里使用SoftOMXComponent的代码作为例子;在硬件解码器的代码里也有类似的代码:

Android

各种必要的函数需要被提供。这里需要关注的就是AllocateBuffer函数。这个函数在一些情况之下会被调用用来分配Buffer。

Codec Component初始化完成的时候的时候,Media Framework就会发现Player刚初始化了安全解码器,于是它就会将Secure Video Path上所要用到的组件置上相应的Flag:

Android

在这里几个标志的作用:

kFlagIsSecure标志决定了Input Buffer需要来自安全内存。由于Media Framework并不知道安全内存的具体实现;在遇到需要分配安全内存的情况下,Framework则会去调用Codec Component提供的AllocateBuffer函数。

所有的Surface内存都是由Gralloc来进行分配。kFlagIsGrallocUsageProtected标志决定了当使用Gralloc来分配Surface内存的时候,Gralloc需要支持从安全内存分配器分配内存。使用安全内存的Surface一般称呼为Protected Surface.

kFlagPushBlankBuffersToNativeWindowOnShutdown表示在Surface无效的时候,显示空白的画面;而不是之前尚存在于Surface中的数据。

最终在AllocateBufferWrapper中,Component通过检查secure标志来决定是否要从安全内存中分配一块区域并返回:

Android

安全内存被分配以后,其handle将被在安全解密系统(DRM进程)和多媒体(Media进程)之间传递。安全解密系统通过ION的API可以获得安全内存的地址,来进行解密操作。而Codec的驱动也可以获得安全内存的地址,将其作为DMA地址来进行解码。

图形和显示系统和Gralloc

要点:

实现支持安全复合的硬件显示设备(HwComposor)

在Gralloc()分配安全内存给具有GRALLOC_USAGE_PROTECTED标志的分配请求

如果不能实现安全的GPU,则将GPU隔离在Secure Video Path之外

解码后用于显示的Surface由SurfaceFlinger进程创建而来。在解码器组件被实例化以后,所需要分配的Surface被放置上了保护flag:

Android

这个保护flag最后在分配Surface所需要使用的内存的时候,会被传递到Gralloc模块里。Gralloc模块负责分配所有与显示相关的内存。在Gralloc模块的代码里,会根据传入的flag选择适当的内存分配器。检查到 GRALLOC_USAGE_PROTECTED标志,在本文的例子中,则会去使用ION申请一块安全内存。

硬件复合器负责对硬件的Layer进行复合,并且显示最终结果;其组件名称为HwCompsor;一般存在于系统分区(/vendor/lib/hw/hwcomposer.xxxx.so).GPU则是负责图形绘制和渲染的引擎。使用硬件复合器可以减轻GPU负担。

含有解码后内容的Surface一般直接就会被复合后输出。在以下情况下,GPU会操作这个Surface:

Player对输出的Surface进行了特效或者贴图等后期处理;

Surface所在的Layer (这里为Protected Layer)的特性不符合硬件复合器的要求;复合操作被Reject,GPU将负责这个Layer的复合操作。

在Secure Video Path中硬件复合最好能够被满足;因为软件复合意味着CPU将可以存取Protected Surface的内容。MPU也会拒绝CPU对保护内存的访问。如果不能够被满足,那么使用Secure state CPU来进行复合操作,则会导致整个多媒体框架实现的复杂度。

在Android的Surfaceflinger中,不会对Protected Layer进行复合操作;遇到Protected Layer就会显示黑屏。这也是Surfaceflinger知道自己可能无法访问安全内存而做出的一个保险的行为。

Android

所以想要改动最少的实现Secure Video Path,则这点需要被满足:

确保Protected Layer的特性不会被硬件复合器拒绝。可以使用dumpsys SurfaceFlinger查看原因;如果复合器在dump函数中记录了Reject Reason的话。通常被拒绝的原因是颜色格式不支持;或者要做Downscale。Upscale一般没限制。所以播放的媒体流的分辨率,最好不要超过屏幕的分辨率。

安全内存File Descriptor在进程间的传递

要点:

使用native_handle作为安全内存的Handle类型

除了Codec,DRM安全解密系统也需要在用户端操作安全内存句柄。在Android 7.0 (Android N)开始,DRM Server (mediaDRM所在的进程)和Media Service不在一个进程里;Codec组件无论是自己调用ION接口分配的函数;还是调用一个管理安全内存的动态库分配的函数,安全内存所对应的File Descriptor(以下简称FD)都只在被分配的进程里有效;同样的FD数值被传递到另一个进程会导致得不到安全内存的信息而不能操作。

在Android中,Binder服务可以帮助传递FD去别的进程;它可以在目标进程里映射一个新的FD。在新建一个Parcel的时候,如果类型是BINDER_TYPE_FD,则Binder驱动会映射一个目标FD。

在Codec的内存分配函数AllocateBufferWrapper中,由于它可接受的句柄类型,并不接受FD,只有如下所示的三种类型;所以无法直接返回一个FD给AllocateBufferWrapper的调用者(Media Framework)。

Android

其中Secure Codec所使用的安全内存句柄只能为后面两种。其中,kSecureBufferTypeNativeHandle就是为FD的传递而包裹的一个类型。这个类型可以帮进程传递一个或者多个FD去另一个进程。当Media Framework检测到安全内存类型为kSecureBufferTypeNativeHandle的时候,它会调用相应的处理函数来处理。分配内存的伪代码请参考上方Pseudo AllocateBufferWrapper的代码段部分。在DRM进程里请参考:system/core/include/cutils/native_handle.h里面的函数;基本上只要取出native_handle_t里面FD数组里的成员,就是在当前进程里可以访问的安全内存FD.

硬件所要具备的条件

安全内存的实现,离不开硬件。硬件需要做到以下几点:

每个硬件需要有不同的ID来表示自己。

具有防火墙功能,能够鉴别访问内存的硬件ID,并且根据ID和防火墙规则来处理访问权限。

需要访问普通内存和安全内存的硬件,需要有多种ID,适时切换ID。

能访问安全内存的ID,不能够去访问普通内存;反之亦然。

硬件复合器这样的硬件,不能对两种内存有写权限。

问答

假如一个非安全的解码器假装是安全的解码器,它是否能够偷取信息?只有真正安全的解码器,才能够访问安全内存,这是由MPU所保证的。假如非安全的解码器任意分配了一块内存冒充安全的解码器,安全解密器会检查内存的属性进而发现这种冒用;假如它真的分配了安全内存(安全内存谁都可以分配)但是最终只有HwComposor能够读取内容并且显示;其他的非安全模块均不能存取这块内存。

为何大多使用静态预留的方式实现安全内存?因为预留的方式简单;MPU仅仅使用范围检查就能知道内存的属性;而动态分配安全内存的方法,经常需要修改内存的属性,稍有疏漏就会留下安全漏洞。

后续

DRM本身的意义,越来越薄弱。因为版权保护意识的增强,防范越来越不重要。但是针对DRM保护的技术,继续会产生巨大的用途,比如在隐私保护等领域。举例,人脸识别算法中的视频和中间数据,是有相当的意义来保护它的。Secure Video Path的存在是相当有必要的。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分