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