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