...when playing clear parts of encrypted content. Change-Id: I5fb027d22212f07b43deced2da77c98cb3800e7f
424 lines
15 KiB
Java
424 lines
15 KiB
Java
/*
|
|
* (c)Copyright 2011 Widevine Technologies, Inc
|
|
*/
|
|
|
|
package com.widevine.test;
|
|
|
|
import android.app.TabActivity;
|
|
import android.os.Bundle;
|
|
import android.os.AsyncTask;
|
|
import android.media.MediaDrm;
|
|
import android.media.MediaCrypto;
|
|
import android.media.MediaCodec;
|
|
import android.media.MediaCodecList;
|
|
import android.media.MediaCodec.CryptoInfo;
|
|
import android.media.MediaCodecInfo;
|
|
import android.media.MediaFormat;
|
|
import android.media.MediaCryptoException;
|
|
import android.media.MediaCodec.CryptoException;
|
|
import android.util.Log;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.UUID;
|
|
import java.util.Arrays;
|
|
import java.util.LinkedList;
|
|
import java.util.ListIterator;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.InputStream;
|
|
import java.io.IOException;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.nio.ByteBuffer;
|
|
import java.lang.Exception;
|
|
import java.security.MessageDigest;
|
|
import org.apache.http.client.methods.HttpPost;
|
|
import org.apache.http.client.HttpClient;
|
|
import org.apache.http.client.ClientProtocolException;
|
|
import org.apache.http.entity.ByteArrayEntity;
|
|
import org.apache.http.impl.client.DefaultHttpClient;
|
|
import org.apache.http.HttpResponse;
|
|
import org.apache.http.util.EntityUtils;
|
|
|
|
public class MediaDrmAPITest extends TabActivity {
|
|
private final String TAG = "MediaDrmAPITest";
|
|
|
|
private class PostRequestTask extends AsyncTask<String, Void, Void> {
|
|
private byte[] mDrmRequest;
|
|
private byte[] mResponseBody;
|
|
|
|
public PostRequestTask(byte[] drmRequest) {
|
|
mDrmRequest = drmRequest;
|
|
}
|
|
|
|
protected Void doInBackground(String... urls) {
|
|
mResponseBody = postRequest(urls[0], mDrmRequest);
|
|
Log.d(TAG, "response length=" + mResponseBody.length);
|
|
return null;
|
|
}
|
|
|
|
public byte[] getResponseBody() {
|
|
return mResponseBody;
|
|
}
|
|
}
|
|
|
|
private byte[] postRequest(String url, byte[] drmRequest) {
|
|
Log.d(TAG, "PostRequest url=" + url);
|
|
HttpClient httpclient = new DefaultHttpClient();
|
|
HttpPost httppost = new HttpPost(url);
|
|
|
|
try {
|
|
// Add data
|
|
ByteArrayEntity entity = new ByteArrayEntity(drmRequest);
|
|
entity.setChunked(true);
|
|
httppost.setEntity(entity);
|
|
httppost.setHeader("User-Agent", "Widevine CDM v1.0");
|
|
|
|
// Execute HTTP Post Request
|
|
HttpResponse response = httpclient.execute(httppost);
|
|
|
|
byte[] responseBody;
|
|
int responseCode = response.getStatusLine().getStatusCode();
|
|
if (responseCode == 200) {
|
|
responseBody = EntityUtils.toByteArray(response.getEntity());
|
|
} else {
|
|
Log.d(TAG, "Server returned HTTP error code " + responseCode);
|
|
return null;
|
|
}
|
|
return responseBody;
|
|
|
|
} catch (ClientProtocolException e) {
|
|
e.printStackTrace();
|
|
} catch (IOException e) {
|
|
e.printStackTrace();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static byte[] hex2ba(String s) {
|
|
int len = s.length();
|
|
byte[] data = new byte[len / 2];
|
|
for (int i = 0; i < len; i += 2) {
|
|
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
|
+ Character.digit(s.charAt(i+1), 16));
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// validate the response body and return the drmResponse blob
|
|
private byte[] parseResponseBody(byte[] responseBody) {
|
|
String bodyString = null;
|
|
try {
|
|
bodyString = new String(responseBody, "UTF-8");
|
|
} catch (UnsupportedEncodingException e) {
|
|
e.printStackTrace();
|
|
}
|
|
|
|
if (bodyString == null) {
|
|
return null;
|
|
}
|
|
|
|
if (!bodyString.startsWith("GLS/")) {
|
|
Log.e(TAG, "Invalid response from server, expected GLS/");
|
|
return null;
|
|
}
|
|
if (!bodyString.startsWith("GLS/1.")) {
|
|
Log.e(TAG, "Invalid server version, expected 1.x");
|
|
return null;
|
|
}
|
|
int drmMessageOffset = bodyString.indexOf("\r\n\r\n");
|
|
if (drmMessageOffset == -1) {
|
|
Log.e(TAG, "Invalid server response, could not locate drm message");
|
|
return null;
|
|
}
|
|
return Arrays.copyOfRange(responseBody, drmMessageOffset + 4, responseBody.length);
|
|
}
|
|
|
|
|
|
|
|
static final UUID kWidevineScheme = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
|
|
static final String kClientAuth = "?source=YOUTUBE&video_id=EGHC6OHNbOo&oauth=ya.gtsqawidevine";
|
|
static final String kServerUrl = "https://jmt17.google.com/video-dev/license/GetCencLicense";
|
|
static final String kPort = "80";
|
|
|
|
static final byte[] kKeyId = hex2ba(// blob size and pssh
|
|
"000000347073736800000000" +
|
|
// Widevine system id
|
|
"edef8ba979d64acea3c827dcd51d21ed00000014" +
|
|
// pssh data
|
|
"08011210e02562e04cd55351b14b3d748d36ed8e");
|
|
|
|
/** Called when the activity is first created. */
|
|
@Override
|
|
public void onCreate(Bundle savedInstanceState) {
|
|
|
|
super.onCreate(savedInstanceState);
|
|
setContentView(R.layout.main);
|
|
|
|
listDecoders();
|
|
|
|
testClearContentNoKeys();
|
|
testEncryptedContent();
|
|
}
|
|
|
|
private void testEncryptedContent() {
|
|
try {
|
|
MediaDrm drm = new MediaDrm(kWidevineScheme);
|
|
byte[] sessionId = drm.openSession();
|
|
|
|
doLicenseExchange(drm, sessionId);
|
|
testDecrypt(sessionId);
|
|
|
|
drm.closeSession(sessionId);
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
private void testClearContentNoKeys() {
|
|
try {
|
|
MediaDrm drm = new MediaDrm(kWidevineScheme);
|
|
byte[] sessionId = drm.openSession();
|
|
|
|
testClear(sessionId);
|
|
|
|
drm.closeSession(sessionId);
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
public void doLicenseExchange(MediaDrm drm, byte[] sessionId) throws Exception {
|
|
MediaDrm.KeyRequest drmRequest;
|
|
drmRequest = drm.getKeyRequest(sessionId, kKeyId, "video/avc",
|
|
MediaDrm.MEDIA_DRM_KEY_TYPE_STREAMING, null);
|
|
|
|
PostRequestTask postTask = new PostRequestTask(drmRequest.data);
|
|
postTask.execute(kServerUrl + ":" + kPort + kClientAuth);
|
|
|
|
// wait for post task to complete
|
|
byte[] responseBody;
|
|
long startTime = System.currentTimeMillis();
|
|
do {
|
|
responseBody = postTask.getResponseBody();
|
|
} while (responseBody == null && System.currentTimeMillis() - startTime < 5000);
|
|
|
|
if (responseBody == null) {
|
|
Log.d(TAG, "No response from license server!");
|
|
} else {
|
|
byte[] drmResponse = parseResponseBody(responseBody);
|
|
|
|
drm.provideKeyResponse(sessionId, drmResponse);
|
|
}
|
|
}
|
|
|
|
|
|
private byte[] getTestModeSessionId(byte[] sessionId) {
|
|
String testMode = new String("test_mode");
|
|
byte[] testModeSessionId = new byte[sessionId.length + testMode.length()];
|
|
for (int i = 0; i < sessionId.length; i++) {
|
|
testModeSessionId[i] = sessionId[i];
|
|
}
|
|
for (int i = 0; i < testMode.length(); i++) {
|
|
testModeSessionId[sessionId.length + i] = (byte)testMode.charAt(i);
|
|
}
|
|
return testModeSessionId;
|
|
}
|
|
|
|
// do minimal codec setup to pass an encrypted buffer down the stack to see if it gets
|
|
// decrypted correctly.
|
|
public void testDecrypt(byte[] sessionId) throws Exception {
|
|
Log.i(TAG, "testDecrypt");
|
|
|
|
MediaCrypto crypto;
|
|
try {
|
|
crypto = new MediaCrypto(kWidevineScheme, getTestModeSessionId(sessionId));
|
|
} catch (MediaCryptoException e) {
|
|
throw e;
|
|
}
|
|
|
|
String mime = "video/avc";
|
|
MediaCodec codec;
|
|
if (crypto.requiresSecureDecoderComponent(mime)) {
|
|
codec = MediaCodec.createByCodecName(getSecureDecoderNameForMime(mime));
|
|
} else {
|
|
codec = MediaCodec.createDecoderByType(mime);
|
|
}
|
|
|
|
MediaFormat format = MediaFormat.createVideoFormat(mime, 640, 480);
|
|
codec.configure(format, null, crypto, 0);
|
|
|
|
codec.start();
|
|
|
|
ByteBuffer[] inputBuffers = codec.getInputBuffers();
|
|
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
|
|
|
|
|
|
int index;
|
|
Log.i(TAG, "waiting for buffer...");
|
|
while ((index = codec.dequeueInputBuffer(0 /* timeoutUs */)) < 0) {
|
|
Thread.sleep(10);
|
|
}
|
|
Log.i(TAG, "Got index " + index);
|
|
|
|
LinkedList<TestVector> vectors = TestVectors.GetTestVectors();
|
|
ListIterator<TestVector> iter = vectors.listIterator(0);
|
|
while (iter.hasNext()) {
|
|
TestVector tv = iter.next();
|
|
|
|
CryptoInfo info = new CryptoInfo();
|
|
int clearSizes[] = { tv.mByteOffset };
|
|
int encryptedSizes[] = { tv.mEncryptedBuf.length };
|
|
|
|
info.set(1, clearSizes, encryptedSizes, tv.mKeyID, tv.mIV,
|
|
MediaCodec.CRYPTO_MODE_AES_CTR);
|
|
|
|
byte clearBuf[] = new byte[tv.mByteOffset];
|
|
for (int i = 0; i < clearBuf.length; i++) {
|
|
clearBuf[i] = (byte)i;
|
|
}
|
|
|
|
inputBuffers[index].clear();
|
|
inputBuffers[index].put(clearBuf, 0, clearBuf.length);
|
|
inputBuffers[index].put(tv.mEncryptedBuf, 0, tv.mEncryptedBuf.length);
|
|
|
|
try {
|
|
codec.queueSecureInputBuffer(index, 0 /* offset */, info,
|
|
0 /* sampleTime */, 0 /* flags */);
|
|
} catch (CryptoException e) {
|
|
ByteBuffer refBuffer = ByteBuffer.allocate(clearBuf.length + tv.mClearBuf.length);
|
|
refBuffer.put(clearBuf, 0, clearBuf.length);
|
|
refBuffer.put(tv.mClearBuf, 0, tv.mClearBuf.length);
|
|
|
|
// in test mode, the WV CryptoPlugin throws a CryptoException where the
|
|
// message string contains a SHA256 hash of the decrypted data, for verification
|
|
// purposes.
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
byte[] sha256 = digest.digest(refBuffer.array());
|
|
if (Arrays.equals(sha256, hex2ba(e.getMessage()))) {
|
|
Log.i(TAG, "sha256: " + e.getMessage() + " matches OK");
|
|
} else {
|
|
Log.i(TAG, "MediaCrypto sha256: " + e.getMessage() +
|
|
"does not match test vector sha256: ");
|
|
for (int i = 0; i < sha256.length; i++) {
|
|
System.out.printf("%02x", sha256[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
codec.stop();
|
|
codec.release();
|
|
Log.i(TAG, "testDecrypt: all done!");
|
|
}
|
|
|
|
// do minimal codec setup to pass a clear buffer down the stack to see if it gets
|
|
// passed through correctly.
|
|
public void testClear(byte[] sessionId) throws Exception {
|
|
Log.i(TAG, "testClear");
|
|
|
|
MediaCrypto crypto;
|
|
try {
|
|
crypto = new MediaCrypto(kWidevineScheme, getTestModeSessionId(sessionId));
|
|
} catch (MediaCryptoException e) {
|
|
throw e;
|
|
}
|
|
|
|
String mime = "video/avc";
|
|
MediaCodec codec;
|
|
if (crypto.requiresSecureDecoderComponent(mime)) {
|
|
codec = MediaCodec.createByCodecName(getSecureDecoderNameForMime(mime));
|
|
} else {
|
|
codec = MediaCodec.createDecoderByType(mime);
|
|
}
|
|
|
|
MediaFormat format = MediaFormat.createVideoFormat(mime, 640, 480);
|
|
codec.configure(format, null, crypto, 0);
|
|
|
|
codec.start();
|
|
|
|
ByteBuffer[] inputBuffers = codec.getInputBuffers();
|
|
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
|
|
|
|
|
|
int index;
|
|
Log.i(TAG, "waiting for buffer...");
|
|
while ((index = codec.dequeueInputBuffer(0 /* timeoutUs */)) < 0) {
|
|
Thread.sleep(10);
|
|
}
|
|
Log.i(TAG, "Got index " + index);
|
|
|
|
LinkedList<TestVector> vectors = TestVectors.GetTestVectors();
|
|
ListIterator<TestVector> iter = vectors.listIterator(0);
|
|
while (iter.hasNext()) {
|
|
TestVector tv = iter.next();
|
|
|
|
inputBuffers[index].clear();
|
|
inputBuffers[index].put(tv.mClearBuf, 0, tv.mClearBuf.length);
|
|
|
|
try {
|
|
codec.queueInputBuffer(index, 0 /* offset */, tv.mClearBuf.length,
|
|
0 /* sampleTime */, 0 /* flags */);
|
|
} catch (CryptoException e) {
|
|
ByteBuffer refBuffer = ByteBuffer.allocate(tv.mClearBuf.length);
|
|
refBuffer.put(tv.mClearBuf, 0, tv.mClearBuf.length);
|
|
|
|
// in test mode, the WV CryptoPlugin throws a CryptoException where the
|
|
// message string contains a SHA256 hash of the decrypted data, for verification
|
|
// purposes.
|
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
byte[] sha256 = digest.digest(refBuffer.array());
|
|
if (Arrays.equals(sha256, hex2ba(e.getMessage()))) {
|
|
Log.i(TAG, "sha256: " + e.getMessage() + " matches OK");
|
|
} else {
|
|
Log.i(TAG, "MediaCrypto sha256: " + e.getMessage() +
|
|
"does not match test vector sha256: ");
|
|
for (int i = 0; i < sha256.length; i++) {
|
|
System.out.printf("%02x", sha256[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
codec.stop();
|
|
codec.release();
|
|
Log.i(TAG, "testClear: all done!");
|
|
}
|
|
|
|
private String getSecureDecoderNameForMime(String mime) {
|
|
int n = MediaCodecList.getCodecCount();
|
|
for (int i = 0; i < n; ++i) {
|
|
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
|
|
|
|
if (info.isEncoder()) {
|
|
continue;
|
|
}
|
|
|
|
String[] supportedTypes = info.getSupportedTypes();
|
|
|
|
for (int j = 0; j < supportedTypes.length; ++j) {
|
|
if (supportedTypes[j].equalsIgnoreCase(mime)) {
|
|
return info.getName() + ".secure";
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void listDecoders() {
|
|
int n = MediaCodecList.getCodecCount();
|
|
for (int i = 0; i < n; ++i) {
|
|
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
|
|
|
|
if (info.isEncoder()) {
|
|
continue;
|
|
}
|
|
|
|
String[] supportedTypes = info.getSupportedTypes();
|
|
|
|
Log.i(TAG, "codec: " + info.getName());
|
|
for (int j = 0; j < supportedTypes.length; ++j) {
|
|
Log.i(TAG, " type: " + supportedTypes[j]);
|
|
}
|
|
}
|
|
}
|
|
}
|