aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Bires <jbires@google.com>2022-06-09 06:09:01 -0700
committerMax Bires <jbires@google.com>2022-06-15 13:40:18 -0700
commit49999c1cf2e9a8184c17e3a1bd8efa0728daa66e (patch)
treef131d5188b9beb01833b56afaee16fa025c6eacc
parentb11dcb3187c62017754b4f38e5606a0ee8482d41 (diff)
downloadRemoteProvisioner-49999c1cf2e9a8184c17e3a1bd8efa0728daa66e.tar.gz
Add widevine provisioning functionality.
This change allows RemoteProvisioner to attempt to provision Widevine on behalf of apps that use it. Widevine certs only need to be provisioned one time, so this application attempts to provision them on boot in order to avoid pushing the burden to the apps that actually use widevine. This is done by scheduling a OneTimeWorkRequest if it is determined that provisioning is needed, which will be executed when network is available. The Job will attempt to retry in the event that any failures occur. Bug: 235491155 Test: atest RemoteProvisionerHostTests Change-Id: I1923cfdf05593a22494900f9d71d7238e590b73c Merged-In: I1923cfdf05593a22494900f9d71d7238e590b73c
-rw-r--r--src/com/android/remoteprovisioner/BootReceiver.java11
-rw-r--r--src/com/android/remoteprovisioner/WidevineProvisioner.java191
-rw-r--r--tests/hosttest/AndroidTest.xml4
-rw-r--r--tests/hosttest/src/com/android/remoteprovisioner/hosttest/RemoteProvisionerWidevineTests.java52
-rw-r--r--tests/testapk/Android.bp32
-rw-r--r--tests/testapk/AndroidManifest.xml30
-rw-r--r--tests/testapk/src/com/android/remoteprovisioner/testapk/WidevineTest.java96
7 files changed, 416 insertions, 0 deletions
diff --git a/src/com/android/remoteprovisioner/BootReceiver.java b/src/com/android/remoteprovisioner/BootReceiver.java
index 4aa3dcd..fd9b019 100644
--- a/src/com/android/remoteprovisioner/BootReceiver.java
+++ b/src/com/android/remoteprovisioner/BootReceiver.java
@@ -31,6 +31,7 @@ import android.util.Log;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
+import androidx.work.OneTimeWorkRequest;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
@@ -79,8 +80,18 @@ public class BootReceiver extends BroadcastReceiver {
.enqueueUniquePeriodicWork("ProvisioningJob",
ExistingPeriodicWorkPolicy.REPLACE, // Replace on reboot.
workRequest);
+ if (WidevineProvisioner.isWidevineProvisioningNeeded()) {
+ Log.i(TAG, "WV provisioning needed. Queueing a one-time provisioning job.");
+ OneTimeWorkRequest wvRequest =
+ new OneTimeWorkRequest.Builder(WidevineProvisioner.class)
+ .setConstraints(constraints)
+ .build();
+ WorkManager.getInstance(context).enqueue(wvRequest);
+ }
}
+
+
private int calcNumPotentialKeysToDownload() {
try {
IRemoteProvisioning binder =
diff --git a/src/com/android/remoteprovisioner/WidevineProvisioner.java b/src/com/android/remoteprovisioner/WidevineProvisioner.java
new file mode 100644
index 0000000..d338376
--- /dev/null
+++ b/src/com/android/remoteprovisioner/WidevineProvisioner.java
@@ -0,0 +1,191 @@
+/**
+ * Copyright (C) 2022 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 com.android.remoteprovisioner;
+
+import android.content.Context;
+import android.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.media.UnsupportedSchemeException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Provides the functionality necessary to provision a Widevine instance running Provisioning 4.0.
+ * This class extends the Worker class so that it can be scheduled as a one time work request
+ * in the BootReceiver if the device does need to be provisioned. This can technically be handled
+ * by any application, but is done within this application for convenience purposes.
+ */
+public class WidevineProvisioner extends Worker {
+
+ private static final int MAX_RETRIES = 3;
+ private static final int TIMEOUT_MS = 20000;
+
+ private static final String TAG = "RemoteProvisioningWV";
+
+ private static final byte[] EMPTY_BODY = new byte[0];
+
+ private static final Map<String, String> REQ_PROPERTIES = new HashMap<String, String>();
+ static {
+ REQ_PROPERTIES.put("Accept", "*/*");
+ REQ_PROPERTIES.put("User-Agent", "Widevine CDM v1.0");
+ REQ_PROPERTIES.put("Content-Type", "application/json");
+ REQ_PROPERTIES.put("Connection", "close");
+ }
+
+ public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
+
+ public WidevineProvisioner(@NonNull Context context, @NonNull WorkerParameters params) {
+ super(context, params);
+ }
+
+ private Result retryOrFail() {
+ if (getRunAttemptCount() < MAX_RETRIES) {
+ return Result.retry();
+ } else {
+ return Result.failure();
+ }
+ }
+
+ /**
+ * Overrides the default doWork method to handle checking and provisioning the device's
+ * widevine certificate.
+ */
+ @Override
+ public Result doWork() {
+ Log.i(TAG, "Beginning WV provisioning request. Current attempt: " + getRunAttemptCount());
+ return provisionWidevine();
+ }
+
+ /**
+ * Checks the status of the system in order to determine if stage 1 certificate provisioning
+ * for Provisioning 4.0 needs to be performed.
+ *
+ * @return true if the device supports Provisioning 4.0 and the system ID indicates it has not
+ * yet been provisioned.
+ */
+ public static boolean isWidevineProvisioningNeeded() {
+ try {
+ final MediaDrm drm = new MediaDrm(WidevineProvisioner.WIDEVINE_UUID);
+
+ if (!drm.getPropertyString("provisioningModel").equals("BootCertificateChain")) {
+ // Not a provisioning 4.0 device.
+ Log.i(TAG, "Not a WV provisioning 4.0 device. No provisioning required.");
+ return false;
+ }
+ int systemId = Integer.parseInt(drm.getPropertyString("systemId"));
+ if (systemId != Integer.MAX_VALUE) {
+ Log.i(TAG, "This device has already been provisioned with its WV cert.");
+ // First stage provisioning probably complete
+ return false;
+ }
+ return true;
+ } catch (UnsupportedSchemeException e) {
+ // Suppress the exception. It isn't particularly informative and may confuse anyone
+ // reading the logs.
+ Log.i(TAG, "Widevine not supported. No need to provision widevine certificates.");
+ return false;
+ } catch (Exception e) {
+ Log.e(TAG, "Something went wrong. Will not provision widevine certificates.", e);
+ return false;
+ }
+ }
+
+ /**
+ * Performs the full roundtrip necessary to provision widevine with the first stage cert
+ * in Provisioning 4.0.
+ *
+ * @return A Result indicating whether the attempt succeeded, failed, or should be retried.
+ */
+ public Result provisionWidevine() {
+ try {
+ final MediaDrm drm = new MediaDrm(WIDEVINE_UUID);
+ final MediaDrm.ProvisionRequest request = drm.getProvisionRequest();
+ drm.provideProvisionResponse(fetchWidevineCertificate(request));
+ } catch (UnsupportedSchemeException e) {
+ Log.e(TAG, "WV provisioning unsupported. Should not have been able to get here.", e);
+ return Result.success();
+ } catch (DeniedByServerException e) {
+ Log.e(TAG, "WV server denied the provisioning request.", e);
+ return Result.failure();
+ } catch (IOException e) {
+ Log.e(TAG, "WV Provisioning failed.", e);
+ return retryOrFail();
+ } catch (Exception e) {
+ Log.e(TAG, "Safety catch-all in case of an unexpected run time exception:", e);
+ return retryOrFail();
+ }
+ Log.i(TAG, "Provisioning successful.");
+ return Result.success();
+ }
+
+ private byte[] fetchWidevineCertificate(MediaDrm.ProvisionRequest req) throws IOException {
+ final byte[] data = req.getData();
+ final String signedUrl = String.format(
+ "%s&signedRequest=%s",
+ req.getDefaultUrl(),
+ new String(data));
+ return sendNetworkRequest(signedUrl);
+ }
+
+ private byte[] sendNetworkRequest(String url) throws IOException {
+ HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
+ con.setRequestMethod("POST");
+ con.setDoOutput(true);
+ con.setDoInput(true);
+ con.setConnectTimeout(TIMEOUT_MS);
+ con.setReadTimeout(TIMEOUT_MS);
+ con.setChunkedStreamingMode(0);
+ for (Map.Entry<String, String> prop : REQ_PROPERTIES.entrySet()) {
+ con.setRequestProperty(prop.getKey(), prop.getValue());
+ }
+
+ try (OutputStream os = con.getOutputStream()) {
+ os.write(EMPTY_BODY);
+ }
+ if (con.getResponseCode() != 200) {
+ Log.e(TAG, "Server request for WV certs failed. Error: " + con.getResponseCode());
+ throw new IOException("Failed to request WV certs. Error: " + con.getResponseCode());
+ }
+
+ BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
+ ByteArrayOutputStream respBytes = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int read = 0;
+ while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
+ respBytes.write(buffer, 0, read);
+ }
+ byte[] respData = respBytes.toByteArray();
+ if (respData.length == 0) {
+ Log.e(TAG, "WV server returned an empty response.");
+ throw new IOException("WV server returned an empty response.");
+ }
+ return respData;
+ }
+}
diff --git a/tests/hosttest/AndroidTest.xml b/tests/hosttest/AndroidTest.xml
index 2365fd0..f701cff 100644
--- a/tests/hosttest/AndroidTest.xml
+++ b/tests/hosttest/AndroidTest.xml
@@ -22,6 +22,10 @@
<option name="cleanup-apks" value="true" />
<option name="test-file-name" value="RemoteProvisionerUnitTests.apk" />
</target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="RemoteProvisionerTestApk.apk" />
+ </target_preparer>
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
<option name="jar" value="RemoteProvisionerHostTests.jar" />
</test>
diff --git a/tests/hosttest/src/com/android/remoteprovisioner/hosttest/RemoteProvisionerWidevineTests.java b/tests/hosttest/src/com/android/remoteprovisioner/hosttest/RemoteProvisionerWidevineTests.java
new file mode 100644
index 0000000..a1adff9
--- /dev/null
+++ b/tests/hosttest/src/com/android/remoteprovisioner/hosttest/RemoteProvisionerWidevineTests.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 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 com.android.remoteprovisioner.hosttest;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+public final class RemoteProvisionerWidevineTests extends BaseHostJUnit4Test {
+ private static final String TEST_PACKAGE_NAME = "com.android.remoteprovisioner.testapk";
+ private static final String WV_CERT_LOCATION = "/data/vendor/mediadrm/IDM1013/L1/oemcert.bin";
+
+ private void deleteWidevineCert() throws Exception {
+ assertTrue("Test requires ability to get root.", getDevice().enableAdbRoot());
+ getDevice().executeShellCommand("rm " + WV_CERT_LOCATION);
+ }
+
+ private void runTest(String testClassName, String testMethodName) throws Exception {
+ testClassName = TEST_PACKAGE_NAME + "." + testClassName;
+ assertTrue(runDeviceTests(TEST_PACKAGE_NAME, testClassName, testMethodName));
+ }
+
+ @Test
+ public void testIfProvisioningNeededIsConsistentWithSystemStatus() throws Exception {
+ runTest("WidevineTest", "testIfProvisioningNeededIsConsistentWithSystemStatus");
+ }
+
+ @Test
+ public void testWipeAndReprovisionCert() throws Exception {
+ deleteWidevineCert();
+ runTest("WidevineTest", "testProvisionWidevine");
+ }
+}
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
new file mode 100644
index 0000000..6dc5dfd
--- /dev/null
+++ b/tests/testapk/Android.bp
@@ -0,0 +1,32 @@
+// Copyright (C) 2022 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "RemoteProvisionerTestApk",
+ srcs: ["src/**/*.java"],
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.rules",
+ "androidx.work_work-runtime",
+ "androidx.work_work-testing",
+ "platform-test-annotations",
+ "cbor-java",
+ ],
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ instrumentation_for: "RemoteProvisioner",
+}
diff --git a/tests/testapk/AndroidManifest.xml b/tests/testapk/AndroidManifest.xml
new file mode 100644
index 0000000..1ce5715
--- /dev/null
+++ b/tests/testapk/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.remoteprovisioner.testapk">
+
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.remoteprovisioner"
+ android:label="RemoteProvisioner app unit tests" />
+</manifest>
diff --git a/tests/testapk/src/com/android/remoteprovisioner/testapk/WidevineTest.java b/tests/testapk/src/com/android/remoteprovisioner/testapk/WidevineTest.java
new file mode 100644
index 0000000..9932cc9
--- /dev/null
+++ b/tests/testapk/src/com/android/remoteprovisioner/testapk/WidevineTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2021 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 com.android.remoteprovisioner.testapk;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.media.MediaDrm;
+import android.media.UnsupportedSchemeException;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.work.ListenableWorker;
+import androidx.work.Worker;
+import androidx.work.testing.TestWorkerBuilder;
+
+import com.android.remoteprovisioner.WidevineProvisioner;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executors;
+
+@RunWith(AndroidJUnit4.class)
+public class WidevineTest {
+
+ private static boolean sSupportsWidevine = true;
+ private static final String TAG = "RemoteProvisionerWidevineTest";
+ private static MediaDrm sDrm;
+
+ @BeforeClass
+ public static void init() throws Exception {
+ try {
+ sDrm = new MediaDrm(WidevineProvisioner.WIDEVINE_UUID);
+ } catch (UnsupportedSchemeException e) {
+ Log.i(TAG, "Device doesn't support widevine, all tests should pass.");
+ sSupportsWidevine = false;
+ }
+ }
+
+ private boolean isProvisioning4() {
+ if (!sDrm.getPropertyString("provisioningModel").equals("BootCertificateChain")) {
+ // Not a provisioning 4.0 device.
+ return false;
+ }
+ return true;
+ }
+
+ private boolean isProvisioned() {
+ int systemId = Integer.parseInt(sDrm.getPropertyString("systemId"));
+ if (systemId != Integer.MAX_VALUE) {
+ return true;
+ }
+ return false;
+ }
+
+ @Test
+ public void testIfProvisioningNeededIsConsistentWithSystemStatus() {
+ if (!sSupportsWidevine) return;
+ assertEquals(isProvisioning4() && !isProvisioned(),
+ WidevineProvisioner.isWidevineProvisioningNeeded());
+ }
+
+ @Test
+ public void testProvisionWidevine() {
+ if (!sSupportsWidevine) return;
+ if (!isProvisioning4()) {
+ Log.i(TAG, "Not a provisioning 4.0 device.");
+ return;
+ }
+ WidevineProvisioner prov = TestWorkerBuilder.from(
+ ApplicationProvider.getApplicationContext(),
+ WidevineProvisioner.class,
+ Executors.newSingleThreadExecutor()).build();
+ assertFalse(isProvisioned());
+ assertEquals(ListenableWorker.Result.success(), prov.doWork());
+ assertTrue(isProvisioned());
+ }
+}