CTS test for libmedia OOB write anywhere
Set up a DRM session, spoof the non-secure device as
secure and pass an invalid pointer as the decrypt
destination buffer address which causes a segfault in
mediaserver on devices without the patch.
This test only runs on non-secure (L3) devices, it is
skipped on secure devices since the vulnerability doesn't
exist there.
b/23223325
Change-Id: Ie88a09ffa9ff722d2fb4642c6655c95cc2478eed
diff --git a/tests/tests/security/jni/android_security_cts_MediaCryptoTest.cpp b/tests/tests/security/jni/android_security_cts_MediaCryptoTest.cpp
new file mode 100644
index 0000000..abb26eb
--- /dev/null
+++ b/tests/tests/security/jni/android_security_cts_MediaCryptoTest.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* Original code copied from NDK Native-media sample code */
+
+//#define LOG_NDEBUG 0
+#define TAG "NativeMediaCrypto"
+#include <log/log.h>
+
+#include <android_media_MediaCrypto.h>
+#include <assert.h>
+#include <binder/MemoryDealer.h>
+#include <jni.h>
+#include <media/ICrypto.h>
+#include <media/stagefright/foundation/AString.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <utils/StrongPointer.h>
+#include <semaphore.h>
+
+
+static const size_t kBufferSize = 1024;
+
+using namespace android;
+
+static jboolean testCrypto(sp<ICrypto> icrypto,
+ const CryptoPlugin::SubSample *subSample, CryptoPlugin::Mode mode)
+{
+ // Allocate source buffer
+ sp<MemoryDealer> memDealer = new MemoryDealer(kBufferSize, "MediaCryptoTest");
+ sp<IMemory> srcBuffer = memDealer->allocate(kBufferSize);
+ if (!srcBuffer->pointer()) {
+ ALOGE("Failed to allocate source buffer");
+ return false;
+ }
+ memset(srcBuffer->pointer(), 's', kBufferSize);
+
+ // Invalid dest pointer should fault if mediaserver attempts
+ // to write to it. Don't use NULL because that's probably
+ // checked for.
+ void *dstPtr = reinterpret_cast<void *>(1);
+
+ // Spoof the device as being secure
+ bool secure = true;
+
+ uint8_t key[16] = {0};
+ uint8_t iv[16] = {0};
+ uint32_t offset = 0;
+ AString errorDetailMsg;
+
+ ssize_t result = icrypto->decrypt(secure, key, iv, mode, srcBuffer, offset,
+ subSample, 1, dstPtr, &errorDetailMsg);
+
+ // call should return an error and shouldn't kill media server
+ return (result != OK && result != DEAD_OBJECT);
+}
+
+// Test for icrypto interface vulnerabilities
+extern "C" jboolean Java_android_security_cts_MediaCryptoTest_validateCryptoNative(JNIEnv *env,
+ jclass /*clazz*/, jobject crypto)
+{
+ bool result = false;
+ sp<ICrypto> icrypto = JCrypto::GetCrypto(env, crypto);
+ if (icrypto != NULL) {
+ if (icrypto->requiresSecureDecoderComponent("video/avc")) {
+ ALOGI("device is secure, bypassing test");
+ return true;
+ }
+
+ CryptoPlugin::Mode unencryptedMode = CryptoPlugin::kMode_Unencrypted;
+ CryptoPlugin::Mode aesCtrMode = CryptoPlugin::kMode_AES_CTR;
+
+ CryptoPlugin::SubSample clrSubSample = {kBufferSize, 0};
+ CryptoPlugin::SubSample encSubSample = {0, kBufferSize};
+
+ result =
+ testCrypto(icrypto, &clrSubSample, unencryptedMode) &&
+ testCrypto(icrypto, &clrSubSample, aesCtrMode) &&
+ testCrypto(icrypto, &encSubSample, unencryptedMode) &&
+ testCrypto(icrypto, &encSubSample, aesCtrMode);
+ } else {
+ ALOGE("Failed to get icrypto interface");
+ }
+ return result;
+}
+
+
+
diff --git a/tests/tests/security/src/android/security/cts/HttpPost.java b/tests/tests/security/src/android/security/cts/HttpPost.java
new file mode 100644
index 0000000..7f855d9
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/HttpPost.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.security.cts;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * HttpPost utility functions.
+ */
+public final class HttpPost {
+ private static final String TAG = "HttpPost";
+
+ private HttpPost() {}
+
+ /**
+ * Executes a post request using {@link HttpURLConnection}.
+ *
+ * @param url The request URL.
+ * @param data The request body, or null.
+ * @param requestProperties Request properties, or null.
+ * @return The response code and body.
+ * @throws IOException If an error occurred making the request.
+ */
+ public static Pair<Integer, byte[]> execute(String url, byte[] data,
+ Map<String, String> requestProperties) throws IOException {
+ HttpURLConnection urlConnection = null;
+ try {
+ urlConnection = (HttpURLConnection) new URL(url).openConnection();
+ urlConnection.setRequestMethod("POST");
+ urlConnection.setDoOutput(data != null);
+ urlConnection.setDoInput(true);
+ urlConnection.setConnectTimeout(5000);
+ urlConnection.setReadTimeout(5000);
+ if (requestProperties != null) {
+ for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
+ urlConnection.setRequestProperty(requestProperty.getKey(),
+ requestProperty.getValue());
+ }
+ }
+ // Write the request body, if there is one.
+ if (data != null) {
+ OutputStream out = urlConnection.getOutputStream();
+ try {
+ out.write(data);
+ } finally {
+ out.close();
+ }
+ }
+ // Read the response code.
+ int responseCode = urlConnection.getResponseCode();
+ // Read the response body.
+ InputStream inputStream = urlConnection.getInputStream();
+ try {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byte scratch[] = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(scratch)) != -1) {
+ byteArrayOutputStream.write(scratch, 0, bytesRead);
+ }
+ byte[] responseBody = byteArrayOutputStream.toByteArray();
+ Log.d(TAG, "responseCode=" + responseCode + ", length=" + responseBody.length);
+ return Pair.create(responseCode, responseBody);
+ } finally {
+ inputStream.close();
+ }
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ }
+ }
+
+}
diff --git a/tests/tests/security/src/android/security/cts/KeyRequester.java b/tests/tests/security/src/android/security/cts/KeyRequester.java
new file mode 100644
index 0000000..26a3337
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/KeyRequester.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import android.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class KeyRequester {
+ private final String TAG = "KeyRequester";
+ private final int MAX_RETRY_COUNT = 3;
+ private final int POOL_SIZE = 1;
+ private final int POOL_TERMINATION_MS_TIMEOUT = 3000; // in milliseconds
+ private final int KEYREQUEST_MS_TIMEOUT = 5000; // in milliseconds
+
+ private byte[] mPssh;
+ private ExecutorService mExecutorService;
+ private Future<byte[]> mFuture;
+ private String mDefaultHeartbeatUrl;
+ private String mServerUrl;
+
+ public KeyRequester(byte[] pssh, String url) {
+ mPssh = pssh;
+ mServerUrl = url;
+ }
+
+ public final String getDefaultHeartbeatUrl() {
+ return mDefaultHeartbeatUrl;
+ }
+
+ public byte[] doTransact(final MediaDrm drm, final byte[] sessionId, final int keyType) {
+ boolean retryRequest;
+ boolean retryTransaction;
+ byte[] keySetIdResult;
+ int getKeyRequestRetryCount;
+ int provisioningRetryCount = 0;
+ MediaDrm.KeyRequest drmRequest;
+
+ mExecutorService = Executors.newFixedThreadPool(POOL_SIZE);
+
+ do {
+ drmRequest = null;
+ getKeyRequestRetryCount = 0;
+ keySetIdResult = null;
+ retryTransaction = false;
+
+ do {
+ retryRequest = false;
+
+ try {
+ drmRequest = drm.getKeyRequest(sessionId, mPssh,
+ "video/avc", keyType, null);
+ } catch (NotProvisionedException e) {
+ Log.i(TAG, "Invalid certificate, reprovisioning");
+ ProvisionRequester provisionRequester = new ProvisionRequester();
+ provisionRequester.doTransact(drm);
+ retryRequest = true;
+ }
+ } while (retryRequest && ++getKeyRequestRetryCount < MAX_RETRY_COUNT);
+
+ if (drmRequest == null) {
+ Log.e(TAG, "Failed to get key request");
+ return null;
+ }
+
+ try {
+ mFuture = mExecutorService.submit(new KeyRequesterTask(mServerUrl, drmRequest));
+ } catch (RejectedExecutionException e) {
+ Log.e(TAG, "Failed to submit KeyRequesterTask for execution", e);
+ if (++provisioningRetryCount < MAX_RETRY_COUNT) {
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ try {
+ byte[] responseBody = mFuture.get(KEYREQUEST_MS_TIMEOUT, TimeUnit.MILLISECONDS);
+ if (responseBody == null) {
+ Log.e(TAG, "No response from license server!");
+ retryTransaction = true;
+ } else {
+ byte[] drmResponse = parseResponseBody(responseBody);
+ try {
+ keySetIdResult = drm.provideKeyResponse(sessionId, drmResponse);
+ } catch (NotProvisionedException e) {
+ Log.i(TAG, "Response invalidated the certificate, reprovisioning");
+ ProvisionRequester provisionRequester = new ProvisionRequester();
+ provisionRequester.doTransact(drm);
+ retryTransaction = true;
+ } catch (DeniedByServerException e) {
+ // informational, the event handler will take care of provisioning
+ Log.i(TAG, "Server rejected the key request");
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "provideKeyResponse failed", e);
+ }
+
+ try {
+ // first call to getKeyRequest does not return heartbeat url
+ drmRequest = drm.getKeyRequest(sessionId, mPssh, "video/avc",
+ keyType, null);
+ try {
+ mDefaultHeartbeatUrl = drmRequest.getDefaultUrl();
+ } catch (Exception e) {
+ // ignore
+ }
+ } catch (NotProvisionedException e) {
+ Log.e(TAG, "Fails to get heartbeat url");
+ }
+ break;
+ }
+ } catch (ExecutionException | InterruptedException ex) {
+ Log.e(TAG, "Failed to execute KeyRequesterTask", ex);
+ shutdownAndAwaitTermination(mExecutorService);
+ return null;
+ } catch (TimeoutException te) {
+ // The request timed out. The network is possibly too slow.
+ // Cancel the running task.
+ Log.d(TAG, "Request timed out, retry...");
+ mFuture.cancel(true);
+ retryTransaction = true;
+ }
+ } while (retryTransaction && ++provisioningRetryCount < MAX_RETRY_COUNT);
+
+ shutdownAndAwaitTermination(mExecutorService);
+ return keySetIdResult;
+ }
+
+ private void shutdownAndAwaitTermination(ExecutorService pool) {
+ pool.shutdown(); // disable new tasks from being submitted
+ try {
+ // wait for existing tasks to terminate
+ if (!pool.awaitTermination(POOL_TERMINATION_MS_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ pool.shutdownNow();
+ // wait for tasks to respond to being cancelled
+ if (!pool.awaitTermination(POOL_TERMINATION_MS_TIMEOUT, TimeUnit.MILLISECONDS))
+ Log.e(TAG, "Pool did not terminate");
+ }
+ } catch (InterruptedException ie) {
+ // (Re-)Cancel if current thread also interrupted
+ pool.shutdownNow();
+ // Preserve interrupt status
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ // 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/")) {
+ 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;
+ }
+ responseBody = Arrays.copyOfRange(responseBody, drmMessageOffset + 4,
+ responseBody.length);
+ }
+ return responseBody;
+ }
+}
diff --git a/tests/tests/security/src/android/security/cts/KeyRequesterTask.java b/tests/tests/security/src/android/security/cts/KeyRequesterTask.java
new file mode 100644
index 0000000..c4d96dd
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/KeyRequesterTask.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import android.media.MediaDrm;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import java.util.HashMap;
+
+public class KeyRequesterTask implements Callable<byte[]> {
+ private static final String TAG = "KeyRequesterTask";
+ private final MediaDrm.KeyRequest mDrmRequest;
+ private final String mUrl;
+
+ public KeyRequesterTask(String url, MediaDrm.KeyRequest drmRequest) {
+ mDrmRequest = drmRequest;
+ mUrl = url;
+ }
+
+ /**
+ * @return a byte array containing the license response if successful,
+ * {@code null} otherwise.
+ */
+ @Override
+ public byte[] call() throws Exception {
+ byte[] drmRequest = mDrmRequest.getData();
+ Log.d(TAG, "PostRequest:" + mUrl);
+
+ HashMap<String, String> headers = new HashMap<>();
+ headers.put("User-Agent", "Widevine CDM v1.0");
+ headers.put("Connection", "close");
+
+ try {
+ Pair<Integer, byte[]> response = HttpPost.execute(mUrl, drmRequest, headers);
+ int responseCode = response.first;
+ if (responseCode != 200) {
+ Log.d(TAG, "Server returned HTTP error code " + responseCode);
+ return null;
+ }
+ return response.second;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+}
+
diff --git a/tests/tests/security/src/android/security/cts/MediaCryptoTest.java b/tests/tests/security/src/android/security/cts/MediaCryptoTest.java
new file mode 100644
index 0000000..b5639a7
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/MediaCryptoTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.cts;
+
+import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.MediaDrmException;
+import android.media.NotProvisionedException;
+import android.media.ResourceBusyException;
+import android.test.AndroidTestCase;
+import android.util.Log;
+import java.util.UUID;
+
+public class MediaCryptoTest extends AndroidTestCase {
+ private static final String TAG = "MediaCryptoTest";
+
+ private static final UUID CLEARKEY_SCHEME_UUID =
+ new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL);
+ private static final UUID WIDEVINE_SCHEME_UUID =
+ new UUID(0xedef8ba979d64aceL, 0xa3c827dcd51d21edL);
+
+ static {
+ System.loadLibrary("ctssecurity_jni");
+ }
+
+ private native boolean validateCryptoNative(MediaCrypto crypto);
+
+ public void testMediaCryptoClearKey() throws Exception {
+ MediaCrypto crypto = null;
+ if (!MediaDrm.isCryptoSchemeSupported(CLEARKEY_SCHEME_UUID)) {
+ Log.i(TAG, "No ClearKey plugin, skipping test");
+ return;
+ }
+ try {
+ byte[] initData = new byte[0];
+ crypto = new MediaCrypto(CLEARKEY_SCHEME_UUID, initData);
+ } catch (MediaCryptoException e) {
+ throw new Error("Failed to create MediaCrypto using ClearKey plugin");
+ }
+
+ assertTrue("MediaCrypto validation failed", validateCryptoNative(crypto));
+ }
+
+ public void testMediaCryptoWidevine() throws Exception {
+ if (!MediaDrm.isCryptoSchemeSupported(WIDEVINE_SCHEME_UUID)) {
+ Log.i(TAG, "No Widevine plugin, skipping test");
+ return;
+ }
+
+ MediaDrm drm = null;
+ byte[] sessionId = null;
+
+ try {
+ drm = new MediaDrm(WIDEVINE_SCHEME_UUID);
+ sessionId = openSession(drm);
+ getWidevineKeys(drm, sessionId);
+ MediaCrypto crypto = new MediaCrypto(WIDEVINE_SCHEME_UUID, sessionId);
+ assertTrue("MediaCrypto validation failed", validateCryptoNative(crypto));
+ } catch (MediaCryptoException | MediaDrmException e) {
+ if (drm != null && sessionId != null) {
+ drm.closeSession(sessionId);
+ }
+ throw e;
+ }
+ }
+
+ private byte[] openSession(MediaDrm drm) throws Exception {
+ byte[] sessionId = null;
+ int retryCount = 3;
+ while (retryCount-- > 0) {
+ try {
+ return drm.openSession();
+ } catch (NotProvisionedException e) {
+ Log.i(TAG, "Missing certificate, provisioning");
+ ProvisionRequester provisionRequester = new ProvisionRequester();
+ provisionRequester.doTransact(drm);
+ } catch (ResourceBusyException e) {
+ Log.w(TAG, "Resource busy in openSession, retrying...");
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ie) {
+ // ignore
+ }
+ }
+ }
+ throw new Error("Failed to open session");
+ }
+
+ private void getWidevineKeys(MediaDrm drm, byte[] sessionId) throws Exception {
+ final String kKeyServerUrl = "https://jmt17.google.com/video/license/GetCencLicense";
+ final byte[] kPssh = hex2ba("08011210e02562e04cd55351b14b3d748d36ed8e");
+ final String kClientAuth = "?source=YOUTUBE&video_id=EGHC6OHNbOo&oauth=ya.gtsqawidevine";
+ final String kPort = "80";
+ KeyRequester keyRequester = new KeyRequester(kPssh, kKeyServerUrl + ":" + kPort + kClientAuth);
+ if (keyRequester.doTransact(drm, sessionId, MediaDrm.KEY_TYPE_STREAMING) == null) {
+ throw new Error("Failed to get keys from license server!");
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/tests/tests/security/src/android/security/cts/ProvisionRequester.java b/tests/tests/security/src/android/security/cts/ProvisionRequester.java
new file mode 100644
index 0000000..ee1cb9e
--- /dev/null
+++ b/tests/tests/security/src/android/security/cts/ProvisionRequester.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.security.cts;
+
+import android.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.util.HashMap;
+
+public class ProvisionRequester {
+ private final String TAG = "ProvisionRequester";
+
+ public ProvisionRequester() {
+ }
+
+ public void doTransact(final MediaDrm drm) {
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ MediaDrm.ProvisionRequest drmRequest;
+ drmRequest = drm.getProvisionRequest();
+ byte[] responseBody = postRequest(drmRequest.getDefaultUrl(),
+ drmRequest.getData());
+
+ if (responseBody == null) {
+ Log.e(TAG, "No response from provisioning server!");
+ } else {
+ try {
+ drm.provideProvisionResponse(responseBody);
+ } catch (DeniedByServerException e) {
+ Log.e(TAG, "Server denied provisioning request");
+ }
+ }
+ }
+ };
+ t.start();
+
+ try {
+ t.join();
+ } catch (InterruptedException e) {
+ }
+ }
+
+ // TODO May want to throw exceptions without having try/catch in body.
+ private byte[] postRequest(String url, byte[] drmRequest) {
+ String signedUrl = url + "&signedRequest=" + new String(drmRequest);
+ Log.d(TAG, "PostRequest:" + signedUrl);
+
+ HashMap<String, String> headers = new HashMap<>();
+ headers.put("Accept", "*/*");
+ headers.put("User-Agent", "Widevine CDM v1.0");
+ headers.put("Content-Type", "application/json");
+ headers.put("Connection", "close");
+
+ try {
+ Pair<Integer, byte[]> response = HttpPost.execute(signedUrl, null, headers);
+ int responseCode = response.first;
+ if (responseCode != 200) {
+ Log.e(TAG, "Server returned HTTP error code " + responseCode);
+ return null;
+ }
+ return response.second;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private void sleep(int msec) {
+ try {
+ Thread.sleep(msec);
+ } catch (InterruptedException e) {
+ }
+ }
+}