diff options
author | Max Bires <jbires@google.com> | 2022-06-15 21:21:04 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-06-15 21:21:04 +0000 |
commit | 06d16c8031ce54bff1a07cc64f8b02140a3753c1 (patch) | |
tree | 44d3e033d966683c80be5d9ebf4b1bc30df57c62 | |
parent | 5058c953044fa00a960fc21854544a14de6a5d2b (diff) | |
parent | 80457a80ab2250beb946dea6d375e3795cf5c28d (diff) | |
download | RemoteProvisioner-android13-qpr2-s9-release.tar.gz |
Add widevine provisioning functionality. am: 406a0f5ff3 am: 80457a80abandroid-13.0.0_r83android-13.0.0_r82android-13.0.0_r81android-13.0.0_r80android-13.0.0_r79android-13.0.0_r78android-13.0.0_r77android-13.0.0_r76android-13.0.0_r75android-13.0.0_r74android-13.0.0_r73android-13.0.0_r72android-13.0.0_r71android-13.0.0_r70android-13.0.0_r69android-13.0.0_r68android-13.0.0_r67android-13.0.0_r66android-13.0.0_r65android-13.0.0_r64android-13.0.0_r63android-13.0.0_r62android-13.0.0_r61android-13.0.0_r60android-13.0.0_r59android-13.0.0_r58android-13.0.0_r56android-13.0.0_r54android-13.0.0_r53android-13.0.0_r52android-13.0.0_r51android-13.0.0_r50android-13.0.0_r49android-13.0.0_r48android-13.0.0_r47android-13.0.0_r46android-13.0.0_r45android-13.0.0_r44android-13.0.0_r43android-13.0.0_r42android-13.0.0_r41android-13.0.0_r40android-13.0.0_r39android-13.0.0_r38android-13.0.0_r37android-13.0.0_r36android-13.0.0_r35android-13.0.0_r34android-13.0.0_r33android-13.0.0_r32android13-qpr3-s9-releaseandroid13-qpr3-s8-releaseandroid13-qpr3-s7-releaseandroid13-qpr3-s6-releaseandroid13-qpr3-s5-releaseandroid13-qpr3-s4-releaseandroid13-qpr3-s3-releaseandroid13-qpr3-s2-releaseandroid13-qpr3-s14-releaseandroid13-qpr3-s13-releaseandroid13-qpr3-s12-releaseandroid13-qpr3-s11-releaseandroid13-qpr3-s10-releaseandroid13-qpr3-s1-releaseandroid13-qpr3-releaseandroid13-qpr3-c-s8-releaseandroid13-qpr3-c-s7-releaseandroid13-qpr3-c-s6-releaseandroid13-qpr3-c-s5-releaseandroid13-qpr3-c-s4-releaseandroid13-qpr3-c-s3-releaseandroid13-qpr3-c-s2-releaseandroid13-qpr3-c-s12-releaseandroid13-qpr3-c-s11-releaseandroid13-qpr3-c-s10-releaseandroid13-qpr3-c-s1-releaseandroid13-qpr2-s9-releaseandroid13-qpr2-s8-releaseandroid13-qpr2-s7-releaseandroid13-qpr2-s6-releaseandroid13-qpr2-s5-releaseandroid13-qpr2-s3-releaseandroid13-qpr2-s2-releaseandroid13-qpr2-s12-releaseandroid13-qpr2-s11-releaseandroid13-qpr2-s10-releaseandroid13-qpr2-s1-releaseandroid13-qpr2-releaseandroid13-qpr2-b-s1-releaseandroid13-d4-s2-releaseandroid13-d4-s1-releaseandroid13-d4-release
Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/RemoteProvisioner/+/18812091
Change-Id: Iab495594b8dede0deccbb299931050365df00f96
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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()); + } +} |