最近在瞭解Android DRM相關的一些知識,下面轉一個ARM大佬分享的內容:html
前言android
本文簡略地介紹瞭如何在Android下實現DRM(Digital Rights Management, 數字版權管理)以及與其適配的Secure Video Path的要點。但願本文可以省去你們一些閱讀代碼和文檔的時間,幫助接觸Android DRM框架不久的朋友。本人在此次Secure Video Path相關的工做以前並無太多的Android經驗;文章中的名詞和概念等都是我的翻譯而來,有不對的地方請指出。git
背景算法
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)實現。數據結構
DRM Framework架構圖,於2018 Apr 18下載自:https://source.android.com/devices/drm架構
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。
實現過程
對於通用的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.實現一個安全通路使得從解密開始直到被顯示都是安全的。
爲了達到這兩個目的,如下組件須要進行必要的增長或者修改。
安全內存
要點:
實現安全內存分配器(好比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中。下面的例子展現瞭如何添加一個安全解碼器:
其次在Codec的enumerateComponents中,須要在Media Framework中註冊本身所支持的Codec類型。除了一般的decoder和encoder,decoder.secure是須要添加支持的。
Player在根據所須要的解碼器的mimeType,找到可用的Secure Codec之後,會去進行初始化。在初始化函數makeComponentInstance中,須要可以分配Secure Codec實例。通常來講,這個函數能夠和普通的Codec的makeComponentInstance複用;只是發現Codec名稱爲」.secure」結尾的時候,在Codec Component內部的數據結構中置上一個Secure標誌;以便後面分配內存的時候,可以知道當前的Codec Component是否是安全解碼器:
解碼器組件在初始化實例的時候,須要提供實例所支持的接口給Media Framework,這裏使用SoftOMXComponent的代碼做爲例子;在硬件解碼器的代碼裏也有相似的代碼:
各類必要的函數須要被提供。這裏須要關注的就是AllocateBuffer函數。這個函數在一些狀況之下會被調用用來分配Buffer。
Codec Component初始化完成的時候的時候,Media Framework就會發現Player剛初始化了安全解碼器,因而它就會將Secure Video Path上所要用到的組件置上相應的Flag:
在這裏幾個標誌的做用:
kFlagIsSecure標誌決定了Input Buffer須要來自安全內存。因爲Media Framework並不知道安全內存的具體實現;在遇到須要分配安全內存的狀況下,Framework則會去調用Codec Component提供的AllocateBuffer函數。
全部的Surface內存都是由Gralloc來進行分配。kFlagIsGrallocUsageProtected標誌決定了當使用Gralloc來分配Surface內存的時候,Gralloc須要支持從安全內存分配器分配內存。使用安全內存的Surface通常稱呼爲Protected Surface.
kFlagPushBlankBuffersToNativeWindowOnShutdown表示在Surface無效的時候,顯示空白的畫面;而不是以前尚存在於Surface中的數據。
最終在AllocateBufferWrapper中,Component經過檢查secure標誌來決定是否要從安全內存中分配一塊區域並返回:
安全內存被分配之後,其handle將被在安全解密系統(DRM進程)和多媒體(Media進程)之間傳遞。安全解密系統經過ION的API能夠得到安全內存的地址,來進行解密操做。而Codec的驅動也能夠得到安全內存的地址,將其做爲DMA地址來進行解碼。
圖形和顯示系統和Gralloc
要點:
實現支持安全複合的硬件顯示設備(HwComposor)
在Gralloc()分配安全內存給具備GRALLOC_USAGE_PROTECTED標誌的分配請求
若是不能實現安全的GPU,則將GPU隔離在Secure Video Path以外
解碼後用於顯示的Surface由SurfaceFlinger進程建立而來。在解碼器組件被實例化之後,所須要分配的Surface被放置上了保護flag:
這個保護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知道本身可能沒法訪問安全內存而作出的一個保險的行爲。
因此想要改動最少的實現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)。
其中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的存在是至關有必要的。
參考資料
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/