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