• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.rkpdapp.provisioner;
18 
19 import android.content.Context;
20 import android.media.DeniedByServerException;
21 import android.media.MediaDrm;
22 import android.media.UnsupportedSchemeException;
23 import android.os.Build;
24 import android.util.Log;
25 
26 import androidx.annotation.NonNull;
27 import androidx.work.Worker;
28 import androidx.work.WorkerParameters;
29 
30 import java.io.BufferedInputStream;
31 import java.io.ByteArrayOutputStream;
32 import java.io.IOException;
33 import java.io.OutputStream;
34 import java.net.HttpURLConnection;
35 import java.net.SocketTimeoutException;
36 import java.net.URL;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.Map;
40 import java.util.UUID;
41 
42 /**
43  * Provides the functionality necessary to provision a Widevine instance running Provisioning 4.0.
44  * This class extends the Worker class so that it can be scheduled as a one time work request
45  * in the BootReceiver if the device does need to be provisioned. This can technically be handled
46  * by any application, but is done within this application for convenience purposes.
47  */
48 public class WidevineProvisioner extends Worker {
49 
50     private static final int MAX_RETRIES = 3;
51     private static final int TIMEOUT_MS = 20000;
52 
53     private static final String TAG = "RkpdWidevine";
54 
55     private static final byte[] EMPTY_BODY = new byte[0];
56 
57     private static final Map<String, String> REQ_PROPERTIES = new HashMap<>();
58     static {
59         REQ_PROPERTIES.put("Accept", "*/*");
60         REQ_PROPERTIES.put("User-Agent", buildUserAgentString());
61         REQ_PROPERTIES.put("Content-Type", "application/json");
62         REQ_PROPERTIES.put("Connection", "close");
63     }
64 
65     public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
66 
WidevineProvisioner(@onNull Context context, @NonNull WorkerParameters params)67     public WidevineProvisioner(@NonNull Context context, @NonNull WorkerParameters params) {
68         super(context, params);
69     }
70 
buildUserAgentString()71     private static String buildUserAgentString() {
72         ArrayList<String> parts = new ArrayList<>();
73         parts.add("AndroidRemoteProvisioner");
74         parts.add(Build.BRAND);
75         parts.add(Build.MODEL);
76         parts.add(Build.TYPE);
77         parts.add(Build.VERSION.INCREMENTAL);
78         parts.add(Build.ID);
79         return String.join("/", parts);
80     }
81 
retryOrFail()82     private Result retryOrFail() {
83         if (getRunAttemptCount() < MAX_RETRIES) {
84             return Result.retry();
85         } else {
86             return Result.failure();
87         }
88     }
89 
90     /**
91      * Overrides the default doWork method to handle checking and provisioning the device's
92      * widevine certificate.
93      */
94     @Override
doWork()95     public Result doWork() {
96         if (isWidevineProvisioningNeeded()) {
97             Log.i(TAG, "Starting WV provisioning. Current attempt: " + getRunAttemptCount());
98             return provisionWidevine();
99         }
100         return Result.success();
101     }
102 
103     /**
104      * Checks the status of the system in order to determine if stage 1 certificate provisioning
105      * for Provisioning 4.0 needs to be performed.
106      *
107      * @return true if the device supports Provisioning 4.0 and the system ID indicates it has not
108      *         yet been provisioned.
109      */
isWidevineProvisioningNeeded()110     public static boolean isWidevineProvisioningNeeded() {
111         try (MediaDrm drm = new MediaDrm(WIDEVINE_UUID)) {
112             if (!drm.getPropertyString("provisioningModel").equals("BootCertificateChain")) {
113                 // Not a provisioning 4.0 device.
114                 Log.i(TAG, "Not a WV provisioning 4.0 device. No provisioning required.");
115                 return false;
116             }
117             int systemId = Integer.parseInt(drm.getPropertyString("systemId"));
118             if (systemId != Integer.MAX_VALUE) {
119                 Log.i(TAG, "This device has already been provisioned with its WV cert.");
120                 // First stage provisioning probably complete
121                 return false;
122             }
123             return true;
124         } catch (UnsupportedSchemeException e) {
125             // Suppress the exception. It isn't particularly informative and may confuse anyone
126             // reading the logs.
127             Log.i(TAG, "Widevine not supported. No need to provision widevine certificates.");
128             return false;
129         } catch (Exception e) {
130             Log.e(TAG, "Something went wrong. Will not provision widevine certificates.", e);
131             return false;
132         }
133     }
134 
135     /**
136      * Performs the full roundtrip necessary to provision widevine with the first stage cert
137      * in Provisioning 4.0.
138      *
139      * @return A Result indicating whether the attempt succeeded, failed, or should be retried.
140      */
provisionWidevine()141     public Result provisionWidevine() {
142         try {
143             final MediaDrm drm = new MediaDrm(WIDEVINE_UUID);
144             final MediaDrm.ProvisionRequest request = drm.getProvisionRequest();
145             drm.provideProvisionResponse(fetchWidevineCertificate(request));
146         } catch (UnsupportedSchemeException e) {
147             Log.e(TAG, "WV provisioning unsupported. Should not have been able to get here.", e);
148             return Result.success();
149         } catch (DeniedByServerException e) {
150             Log.e(TAG, "WV server denied the provisioning request.", e);
151             return Result.failure();
152         } catch (IOException e) {
153             Log.e(TAG, "WV Provisioning failed.", e);
154             return retryOrFail();
155         } catch (Exception e) {
156             Log.e(TAG, "Safety catch-all in case of an unexpected run time exception:", e);
157             return retryOrFail();
158         }
159         Log.i(TAG, "Provisioning successful.");
160         return Result.success();
161     }
162 
fetchWidevineCertificate(MediaDrm.ProvisionRequest req)163     private byte[] fetchWidevineCertificate(MediaDrm.ProvisionRequest req) throws IOException {
164         final byte[] data = req.getData();
165         final String signedUrl = String.format(
166                 "%s&signedRequest=%s",
167                 req.getDefaultUrl(),
168                 new String(data));
169         try {
170             return sendNetworkRequest(signedUrl);
171         } catch (SocketTimeoutException e) {
172             Log.i(TAG, "Provisioning failed with normal URL, retrying with China URL.");
173             final String chinaUrl = req.getDefaultUrl().replace(".com", ".cn");
174             final String signedUrlChina = String.format(
175                     "%s&signedRequest=%s",
176                     chinaUrl,
177                     new String(data));
178             return sendNetworkRequest(signedUrlChina);
179         }
180     }
181 
sendNetworkRequest(String url)182     private byte[] sendNetworkRequest(String url) throws IOException {
183         HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
184         con.setRequestMethod("POST");
185         con.setDoOutput(true);
186         con.setDoInput(true);
187         con.setConnectTimeout(TIMEOUT_MS);
188         con.setReadTimeout(TIMEOUT_MS);
189         con.setChunkedStreamingMode(0);
190         for (Map.Entry<String, String> prop : REQ_PROPERTIES.entrySet()) {
191             con.setRequestProperty(prop.getKey(), prop.getValue());
192         }
193 
194         try (OutputStream os = con.getOutputStream()) {
195             os.write(EMPTY_BODY);
196         }
197         if (con.getResponseCode() != 200) {
198             Log.e(TAG, "Server request for WV certs failed. Error: " + con.getResponseCode());
199             throw new IOException("Failed to request WV certs. Error: " + con.getResponseCode());
200         }
201 
202         BufferedInputStream inputStream = new BufferedInputStream(con.getInputStream());
203         ByteArrayOutputStream respBytes = new ByteArrayOutputStream();
204         byte[] buffer = new byte[1024];
205         int read;
206         while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
207             respBytes.write(buffer, 0, read);
208         }
209         byte[] respData = respBytes.toByteArray();
210         if (respData.length == 0) {
211             Log.e(TAG, "WV server returned an empty response.");
212             throw new IOException("WV server returned an empty response.");
213         }
214         return respData;
215     }
216 }
217