Deep understanding of DRM - MediaDRM and MediaCrypto

How MediaDRM Works?

Next, take ExoPlayer code as an example to introduce MediaDRM used in Widevine Modular.

To sum up, there are the following steps

  • Step1. Create MeidaDRM instance based on UUID
  • Step2.Open Session
  • Step3. Add keys: getKeyRequest and provideKeyResponse of mediadrm
  • Step4. Create a MediaCrypto object and register it in MediaCodec
  • Step5. After having MediaExtractor, MediaCodec and MediaCrypto, start reading sampledata from the extractor, send it to the Decoder through queueSecureInputBuffer, and then decrypt the content using MediaCodec's decrypt method
  • Last close session

Create MeidaDRM instance based on UUID

com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
try {
 mediaDrm = new MediaDrm(uuid);
...
}
frameworks/av/media/libmediaplayerservice/Drm.cpp@createPlugin(const uint8_t uuid[16])
↓
/*
 * Search the / vendor/lib/mediadrm directory for the plugin that supports the drm specified by the corresponding uuid. The search path is the same regardless of which platform
 */
Drm.cpp@findFactoryForScheme(const uint8_t uuid[16])
↓
/*
 *Confirm whether the so Library in the above directory implements the createDrmFactory method
*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 Inherited from DrmFactory
*Instantiate wvdrmplugin here, which inherits from 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 and DrmPlugin are mentioned above. Draw a simple class diagram as follows

It can be seen that DrmFactory and DrmPlugin have specific subclasses corresponding to specific Drm methods. In addition to judging whether the current Drm method is supported, DrmFactory is also used to instantiate DrmPlugin, and DrmPlugin completes specific decryption, KeyRequest, etc.

Open Session

Generate a unique session id for subsequent operations.

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();
....
 }
}

The next process is shown in the figure below

In the figure, in the QueryKeyControlInfo method of cdmSession, OEMCryptoSessionId will be obtained and written to KeyControlBlockMap.

Add keys

That is, getKeyRequest and provideKeyResponse of MediaDrm.

Let's start with 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);
 }
}

The next process is shown in the figure below

There are three types of cdmlicensetypes: offline: offline License, streaming: online License, release: release the key saved during offline. Accordingly, if it is a streaming or offline key type, it will be bound with the corresponding session id. if it is a release type, the key set id of release is required for binding, and there is no session id at this time.

In the InitializationData construction method, determine the type of stream according to the mimeType. If it is video/mp4 or audio/mp4, it is considered as CENC stream. Then read mp4box and drmInitData from pssh box. If the mimeType is WebM, it is considered as WebM stream.

The EnableTimer method starts timing and is used to count whether the key has expired.

Let's look at provideKeyResponse

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

The next process is shown in the figure below

In the figure, the input parameter of provideKeyResponse method is the byte [] returned by the server.

In the ExtractContentKeys(License) method of CdmLicense, you can read key data\iv and key control data\iv from the license.

In the LoadKeys of cryptoSession, CryptoKeys is assigned to OEMCrypto_KeyObject.

Create a MediaCrypto object and register it in MediaCodec

Create a MediaCrypto object using UUID and SessionID. Use MediaCodec The configure (mediaformat, surface, MediaCrypto, int) method registers the MeidaCrypto object in MediaCodec so that Codec can decrypt the content.

Let's first look at the part of MediaCrypto initialization

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

The next process is shown in the figure below

In the figure, isCryptoSchemeSupport is to see if the uuid passed is the uuid corresponding to widevine.

loadLibraryForScheme(path, uuid) is to find the so Library in the vendor/lib/mediadrm directory to see if the so library implements the createCryptoFactory method.

Next, let's look at mediacodec Configure (mediacrypto) process

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);

Start decryption

After having MediaExtractor, MediaCodec and MediaCrypto, start reading sampledata from the extractor, send it to the Decoder through queueSecureInputBuffer, and then decrypt the content using MediaCodec's decrypt method.

The above steps are repeated

from Sample Readout includes keyid, iv And so on 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()) { //Take fmp4 as an example. As long as the extractor reads the box related to encryption, it will write the corresponding flag to 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;
...
}
stay feedInputBuffer Called when 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);
}

The next process is as follows

In the FindSessionForKey method in the figure, the corresponding session is found by using the corresponding relationship between kid and sid. This corresponding relationship is established when the key response, and the same logic is used in sessionSharing. If the session load ed with the corresponding key is not found, need is returned_ Key, and the error "unable to find session" is reported.

In the decrypt method of cryptoSession, there are two situations: if the flow is clear, directly call OEMCrypto_CopyBuffer and SET_VIDEO_PLAY_START_FLAG is set to true, because according to the CENC standard, the starting position of the encrypted stream is a small clear stream; Oemcrypto is only called if the stream is encrypted_ Decryptctr. If it is found that the key has expired, it will return NEED_KEY..

Widevine DRM Testing

There are many methods to verify whether the DRM works normally:

  • The GTS test includes the test items of two widevine s
  • There is also an exoplayerdemo. Exe in the widevine/libwvdrmengine/test/demo directory Apk is designed for WidevineDRM testing. The test items contained in APK are basically the same as those contained in google's native exoplayer demo
  • Reference Implementation
    It provides reference implementations for all features of OEMCrypto. You can use this part of code for debugging or testing, but there is no production key or level 1 security. Use the following
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 
  • Conduct 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
    This APK uses the MediaDRM API to complete the key request, responds to the key response in CDM, and loads the key into TEE through OEMCrypto API. The APK does not have an interface. You can judge whether the key is successfully loaded through the log.
cd $ANDROID_BUILD_TOP/vendor/widevine/libwvdrmengine/test/java mm
adb install MediaDrmAPITest.apk 

Welcome to my official account fifty, sharing all kinds of audio and video, mobile development knowledge ~

Did the article help you? You can scan the following QR code to reward. You can reward as much as you like~

Keywords: Android Encryption

Added by busnut on Thu, 23 Dec 2021 04:47:05 +0200