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:
@@ -63,6 +63,7 @@ LOCAL_SHARED_LIBRARIES := \
|
||||
liblog \
|
||||
libstlport \
|
||||
libutils \
|
||||
libstagefright_foundation \
|
||||
|
||||
LOCAL_WHOLE_STATIC_LIBRARIES := \
|
||||
cdm_protos
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
613
libwvdrmengine/test/java/src/com/widevine/test/TestVectors.java
Normal file
613
libwvdrmengine/test/java/src/com/widevine/test/TestVectors.java
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user