Add end-to-end decryption test with vectors

Added a test_mode flag to the libwvdrmengine plugin
to support verifying decryption results.

Change-Id: I9edbd6279d54fc495b5bbad8273c179106cad474
This commit is contained in:
Jeff Tinker
2013-04-09 01:57:15 -07:00
parent 826576315c
commit 352e7b0820
7 changed files with 876 additions and 32 deletions

View File

@@ -63,6 +63,7 @@ LOCAL_SHARED_LIBRARIES := \
liblog \
libstlport \
libutils \
libstagefright_foundation \
LOCAL_WHOLE_STATIC_LIBRARIES := \
cdm_protos

View File

@@ -7,6 +7,7 @@ LOCAL_SRC_FILES := \
LOCAL_C_INCLUDES := \
bionic \
external/stlport/stlport \
external/openssl/include \
frameworks/av/include \
frameworks/native/include \
vendor/widevine/libwvdrmengine/cdm/core/include \

View File

@@ -29,7 +29,8 @@ class WVCryptoPlugin : public android::CryptoPlugin {
DISALLOW_EVIL_CONSTRUCTORS(WVCryptoPlugin);
wvcdm::WvContentDecryptionModule* const mCDM;
const wvcdm::CdmSessionId mSessionId;
wvcdm::CdmSessionId mSessionId;
bool mTestMode;
};
} // namespace wvdrm

View File

@@ -10,9 +10,12 @@
#include <string>
#include <vector>
#include <openssl/sha.h>
#include "utils/Errors.h"
#include "utils/String8.h"
#include "wv_cdm_constants.h"
#include "media/stagefright/MediaErrors.h"
namespace wvdrm {
@@ -23,7 +26,14 @@ using namespace wvcdm;
WVCryptoPlugin::WVCryptoPlugin(const void* data, size_t size,
WvContentDecryptionModule* cdm)
: mCDM(cdm),
mSessionId(static_cast<const char*>(data), size) {}
mSessionId(static_cast<const char*>(data), size),
mTestMode(false) {
size_t index = mSessionId.find("test_mode");
if (index != string::npos) {
mSessionId = mSessionId.substr(0, index);
mTestMode = true;
}
}
bool WVCryptoPlugin::requiresSecureDecoderComponent(const char *mime) const {
// TODO: Determine if we are using L1 or L3 and return an appropriate value.
@@ -100,6 +110,27 @@ ssize_t WVCryptoPlugin::decrypt(bool secure, const uint8_t key[KEY_ID_SIZE],
}
}
// In test mode, we return an error that causes a detailed error
// message string containing a SHA256 hash of the decrypted data
// to get passed to the java app via CryptoException. The test app
// can then use the hash to verify that decryption was successful.
if (mTestMode) {
SHA256_CTX ctx;
uint8_t digest[SHA256_DIGEST_LENGTH];
SHA256_Init(&ctx);
SHA256_Update(&ctx, dstPtr, offset);
SHA256_Final(digest, &ctx);
String8 buf;
for (size_t i = 0; i < sizeof(digest); i++) {
buf.appendFormat("%02x", digest[i]);
}
*errorDetailMsg = AString(buf.string());
return ERROR_DRM_VENDOR_MIN;
}
return static_cast<ssize_t>(offset);
}

View File

@@ -8,14 +8,27 @@ 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;
@@ -79,7 +92,7 @@ public class MediaDrmAPITest extends TabActivity {
return null;
}
private static byte[] hexToByteArray(String s) {
private static byte[] hex2ba(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
@@ -125,12 +138,12 @@ public class MediaDrmAPITest extends TabActivity {
static final String kServerUrl = "https://jmt17.google.com/video-dev/license/GetCencLicense";
static final String kPort = "80";
static final byte[] kKeyId = hexToByteArray(// blob size and pssh
"000000347073736800000000" +
// Widevine system id
"edef8ba979d64acea3c827dcd51d21ed00000014" +
// pssh data
"08011210e02562e04cd55351b14b3d748d36ed8e");
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
@@ -138,36 +151,181 @@ public class MediaDrmAPITest extends TabActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
listDecoders();
try {
MediaDrm drm = new MediaDrm(kWidevineScheme);
byte[] sessionId = drm.openSession();
MediaDrm.KeyRequest drmRequest;
drmRequest = drm.getKeyRequest(sessionId, kKeyId, "video/mp4",
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);
drm.closeSession(sessionId);
}
doLicenseExchange(drm, sessionId);
testDecrypt(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 {
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, "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]);
}
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* (c)Copyright 2011 Widevine Technologies, Inc
*/
package com.widevine.test;
import java.util.LinkedList;
import android.util.Log;
public class TestVector {
private final static String TAG = "CENC-TestVector";
public TestVector(String keyID, String iv,
String encBuf, String clrBuf, int offset) {
mKeyID = hex2ba(keyID);
mIV = hex2ba(iv);
mEncryptedBuf = hex2ba(encBuf);
mClearBuf = hex2ba(clrBuf);
mByteOffset = offset;
}
public final byte[] mKeyID;
public final byte[] mIV;
public final byte[] mEncryptedBuf;
public final byte[] mClearBuf;
public final int mByteOffset;
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;
}
};

File diff suppressed because one or more lines are too long