Android DRM

最近在了解Android DRM相关的一些知识,下面转一个ARM大佬分享的内容:html

前言android

本文简略地介绍了如何在Android下实现DRM(Digital Rights Management, 数字版权管理)以及与其适配的Secure Video Path的要点。但愿本文可以省去你们一些阅读代码和文档的时间,帮助接触Android DRM框架不久的朋友。本人在此次Secure Video Path相关的工做以前并无太多的Android经验;文章中的名词和概念等都是我的翻译而来,有不对的地方请指出。git

640?wx_fmt=jpeg

背景算法

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

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

  • 经典的Android DRM Framework (https://source.android.com/devices/drm)架构;服务器

  • 如今用的比较多的mediaDRM (https://developer.android.com/reference/android/media/MediaDrm.html)实现。数据结构

640?wx_fmt=png

DRM Framework架构图,于2018 Apr 18下载自:https://source.android.com/devices/drm架构

640?wx_fmt=png

MediaDrm流程及其工做流程图,于2018 Apr 18下载自:https://developer.android.com/reference/android/media/MediaDrm.htmlapp

这二者的区别是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。

640?wx_fmt=jpeg

实现过程

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

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

 

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

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

 

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

640?wx_fmt=png

为了达到这两个目的,如下组件须要进行必要的增长或者修改。

640?wx_fmt=jpeg

安全内存

要点:

  • 实现安全内存分配器(好比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的问题。条件容许的话,建议你们尽可能更新到后面的版本。

640?wx_fmt=jpeg

安全解密系统

要点:

  • 实如今安全环境里解密而且将结果放入安全内存的操做

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

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

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

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

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

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

640?wx_fmt=jpeg

视频解码器

要点:

  • 修改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中。下面的例子展现了如何添加一个安全解码器:

 

640?wx_fmt=png

其次在Codec的enumerateComponents中,须要在Media Framework中注册本身所支持的Codec类型。除了一般的decoder和encoder,decoder.secure是须要添加支持的。

 

640?wx_fmt=png

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

 

640?wx_fmt=png

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

 

640?wx_fmt=png

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

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

640?wx_fmt=png

在这里几个标志的做用:

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

  • 全部的Surface内存都是由Gralloc来进行分配。kFlagIsGrallocUsageProtected标志决定了当使用Gralloc来分配Surface内存的时候,Gralloc须要支持从安全内存分配器分配内存。使用安全内存的Surface通常称呼为Protected Surface.

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

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

640?wx_fmt=png

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

640?wx_fmt=jpeg

图形和显示系统和Gralloc

要点:

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

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

  • 若是不能实现安全的GPU,则将GPU隔离在Secure Video Path以外

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

640?wx_fmt=png

这个保护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知道本身可能没法访问安全内存而作出的一个保险的行为。

640?wx_fmt=png

因此想要改动最少的实现Secure Video Path,则这点须要被知足:

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

640?wx_fmt=jpeg

安全内存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)。

640?wx_fmt=png

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

640?wx_fmt=jpeg

硬件所要具有的条件

安全内存的实现,离不开硬件。硬件须要作到如下几点:

  • 每一个硬件须要有不一样的ID来表示本身。

  • 具备防火墙功能,可以鉴别访问内存的硬件ID,而且根据ID和防火墙规则来处理访问权限。

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

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

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

问答

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

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

640?wx_fmt=jpeg

后续

DRM自己的意义,愈来愈薄弱。由于版权保护意识的加强,防范愈来愈不重要。可是针对DRM保护的技术,继续会产生巨大的用途,好比在隐私保护等领域。举例,人脸识别算法中的视频和中间数据,是有至关的意义来保护它的。Secure Video Path的存在是至关有必要的。

640?wx_fmt=jpeg

参考资料

    • Android Hardware Composor:
      https://source.android.com/devices/graphics/arch-sf-hwc

    • Arm TZMP1 Slides and Video:
      https://es.slideshare.net/linaroorg/hkg18408-a-drm-solution-using-tzmp
      http://connect.linaro.org/resource/hkg18/hkg18-408/

    • NXP: Secure Data Path work with i.MX8Mhttp://connect.linaro.org/resource/hkg18/hkg18-113/

相关文章
相关标签/搜索