深入理解DRM——MediaDRM和MediaCrypto

Posted zhanghui_cuc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解DRM——MediaDRM和MediaCrypto相关的知识,希望对你有一定的参考价值。

How MediaDRM Works?

下面以ExoPlayer代码为例介绍Widevine Modular中所用的MediaDRM。

总结下来有以下几步

  • Step1.根据UUID创建MeidaDRM实例
  • Step2.Open Session
  • Step3. add keys: MediaDrm的getKeyRequest和provideKeyResponse
  • Step4. 创建MediaCrypto对象,注册到MediaCodec中
  • Step5. 拥有了MediaExtractor,MediaCodec,MediaCrypto之后,就开始从extractor中readSampleData,通过queueSecureInputBuffer送给Decoder,再使用MediaCodec的decrypt方法解密内容.
  • 最后close session

根据UUID创建MeidaDRM实例

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
try {
 mediaDrm = new MediaDrm(uuid);
...
}
frameworks/av/media/libmediaplayerservice/Drm.cpp@createPlugin(const uint8_t uuid[16])/*
 * 在/vendor/lib/mediadrm目录下搜索支持对应uuid所指定drm的plugin,这个搜索的路径无论哪个平台都一样
 */
Drm.cpp@findFactoryForScheme(const uint8_t uuid[16])/*
 *确认上述目录下的so库是否实现了createDrmFactory方法
*createDrmFactory constructs and returns an instance of a DrmFactory object.
*When a match is found, the DrmEngine's createDrmPlugin methods is used to create *DrmPlugin instances to support that DRM scheme.
 */
Drm.cpp@loadLibraryForScheme(const String8 &path, const uint8_t uuid[16])/*
*WVDrmFactory继承自DrmFactory
*在这里实例化WVDrmPlugin, WVDrmPlugin继承自DrmPlugin
*/
vendor/widevine/libwvdrmengine/src/WVDrmFactory.cpp@createDrmPlugin(const uint8_t uuid[16], DrmPlugin** plugin)
↓
vendor/widevine/libwvdrmengine/mediadrm/src/WVDrmPlugin.cpp@WVDrmPlugin(const sp<WvContentDecryptionModule>& cdm, WVGenericCryptoInterface* crypto)

上面提到了DrmFactory和DrmPlugin,画一个简单的类图如下

可以看到,DrmFactory和DrmPlugin对应具体的Drm方法都有具体的子类,DrmFactory除了判断当前Drm方法是否受支持外,还用于实例化DrmPlugin,而DrmPlugin完成具体的解密\KeyRequest等等工作。

Open Session

为后续操作生成唯一的session id。

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
private void openInternal(boolean allowProvisioning) {.
//Open a new session with the MediaDrm object.  A session ID is returned.
   sessionId = mediaDrm.openSession();.
 }
}

再往下的流程如下图所示

图中,在cdmSession的QueryKeyControlInfo方法中会获得OEMCryptoSessionId并且写到KeyControlBlockMap中去。

Add keys

也就是MediaDrm的getKeyRequest和provideKeyResponse。

先来看getKeyRequest

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
private void postKeyRequest(byte[] scope, int keyType) {
 try {
   KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
       optionalKeyRequestParameters);
   postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
 } catch (Exception e) {
   onKeysError(e);
 }
}

再往下的流程如下图所示

CdmLicenseType有三种:offline:离线License, streaming:在线License, release:把offline时保存的key release掉.相应的如果是streaming或者offline的key type,都会与对应的session id绑定,如果是release type的,绑定需要release的key set id,此时没有session id。

在InitializationData的构造方法中,根据mimeType来判断是什么类型的流,如果是video/mp4或audio/mp4,则认为是CENC流,然后去读mp4box,从pssh box读drmInitData,如果mimeType是webm的,则认为是Webm的流。

EnableTimer方法开始计时,用于统计key是否过期。

再来看provideKeyResponse

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);

再往下的流程如下图所示

图中,provideKeyResponse方法的入参就是服务器返回的byte[]。

在CdmLicense的ExtractContentKeys(License)方法中,就是从license中读key data\\iv 和 key control data\\iv。

在cryptoSession的LoadKeys中,就是把CryptoKeys赋值给OEMCrypto_KeyObject。

创建MediaCrypto对象,注册到MediaCodec中

使用UUID和SessionID创建MediaCrypto对象.使用MediaCodec.configure(MediaFormat, Surface, MediaCrypto, int)方法将MeidaCrypto对象注册到MediaCodec中,从而让Codec可以解密内容。

先来看MediaCrypto初始化的部分

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
private void openInternal(boolean allowProvisioning) {
 try {
   sessionId = mediaDrm.openSession();
   mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
   state = STATE_OPENED;.
}

再往下的流程如下图所示

图中isCryptoSchemeSupport就是看传过来的uuid是不是widevine对应的uuid。

loadLibraryForScheme(path, uuid)就是去vendor/lib/mediadrm目录下找so库,看so库是否有实现createCryptoFactory方法。

接下来看Mediacodec.configure(mediaCrypto)的流程

com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
   MediaCrypto crypto) throws DecoderQueryException {
 codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats);
 MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround,
     tunnelingAudiosessionId);
 codec.configure(mediaFormat, surface, crypto, 0);
 if (Util.SDK_INT >= 23 && tunneling) {
   tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
 }
}

frameworks/av/media/libstagefright/MediaCodec.cpp
status_t MediaCodec::configure(
        const sp<AMessage> &format,
        const sp<Surface> &surface,
        const sp<ICrypto> &crypto,
        uint32_t flags) {
…
send msg: kWhatConfigure
...
}

onkWhatConfigure
mCrypto = static_cast<ICrypto *>(crypto);

开始解密

拥有了MediaExtractor,MediaCodec,MediaCrypto之后,就开始从extractor中readSampleData,通过queueSecureInputBuffer送给Decoder,再使用MediaCodec的decrypt方法解密内容。

上述步骤循环往复

Sample中读出包括keyid, iv等在内的cryptoinfo
com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished,
   long decodeOnlyUntilUs) {// Read encryption data if the sample is encrypted.
if (buffer.isEncrypted()) { //以fmp4为例,这里只要extractor读到了跟加密相关的box,就会把对应的flag写true
 readEncryptionData(buffer, extrasHolder);
}}

private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) {// Populate the cryptoInfo.
buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
   extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR);
…
}

public final class CryptoInfo {

 /**
  * @see android.media.MediaCodec.CryptoInfo#iv
  */
 public byte[] iv;
 /**
  * @see android.media.MediaCodec.CryptoInfo#key
  */
 public byte[] key;
 /**
  * @see android.media.MediaCodec.CryptoInfo#mode
  */
 @C.CryptoMode
 public int mode;
 /**
  * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
  */
 public int[] numBytesOfClearData;
 /**
  * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
  */
 public int[] numBytesOfEncryptedData;
 /**
  * @see android.media.MediaCodec.CryptoInfo#numSubSamples
  */
 public int numSubSamples;
…
}
在feedInputBuffer的时候调用queueSecureInputBuffer
if (bufferEncrypted) {
 MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(buffer,
     adaptiveReconfigurationBytes);
 codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
} else {
 codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0);
}

再往下的流程如下所示

图中在FindSessionForKey方法中是利用kid与sid的对应关系找到相应的session,这个对应关系在key response的时候就建立起来了,在sessionSharing中也是这样的逻辑.如果找不到load了相应key的session,则返回NEED_KEY,并且报错"unable to find session"。

在cryptoSession的decrypt方法中会分两种情况:若是清流,则直接调用OEMCrypto_CopyBuffer,并且会将SET_VIDEO_PLAY_START_FLAG置位true,因为根据CENC标准,在加密流起始位置是一小段清流;若是加密流,才会调用OEMCrypto_DecryptCTR.这里如果发现key过期了,会返回NEED_KEY.。

Widevine DRM Testing

验证设备DRM工作是否正常,也有很多中方法:

  • GTS测试中包含了对两种widevine的测项
  • widevine/libwvdrmengine/test/demo目录下还有一个ExoPlayerDemo.apk专供WidevineDRM测试,包含的测项基本和google原生exoplayer demo包含的一样
  • Reference Implementation
    为OEMCrypto的所有特性都提供了参考实现,可以用这部分代码做调试或测试,但是其中没有production key也没有level 1 security,使用如下
cd $ANDROID_BUILD_TOP/vendor/widevine/libwvdrmengine/oemcrypto/mock mm
adb root
adb remount						
adb push $OUT/system/vendor/lib/liboemcrypto.so /system/vendor/lib 
  • 进行unit test
cd $ANDROID_BUILD_TOP/external/gtest
mm
# Build the unit tests for oemcrypto.so:
cd $ANDROID_BUILD_TOP/vendor/widevine/libwvdrmengine/oemcrypto/test mm
						
Run the existing unit tests:
					
cd $ANDROID_BUILD_TOP
adb root
adb remount
adb push $OUT/system/bin/oemcrypto_test /system/bin 
adb shell /system/bin/oemcrypto_test 
  • Build Test APK
    这个APK利用MediaDRM API完成key request,在CDM中响应key response,通过OEMCrypto API将key load到TEE中。这个APK没有界面,通过log可以判断key是否成功load。
cd $ANDROID_BUILD_TOP/vendor/widevine/libwvdrmengine/test/java mm
adb install MediaDrmAPITest.apk 

欢迎关注我的公众号灰度五十,分享各类音视频、移动开发知识~

文章帮到你了?可以扫描如下二维码进行打赏,打赏多少您随意~

以上是关于深入理解DRM——MediaDRM和MediaCrypto的主要内容,如果未能解决你的问题,请参考以下文章

深入理解DRM——直播流中的DRM

深入理解DRM——直播流中的DRM

深入理解DRM——了解Widevine与OEMCrypto

Android MediaDrm 唯一 ID

构建DRM系统的重要基石——EMECDMAESCENC和密钥

【转】DRM (二)基本概念