Skip to content

Commit

Permalink
Work around incorrect ClearKey encoding prior to O-MR1
Browse files Browse the repository at this point in the history
Issue: #3138

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=175006223
  • Loading branch information
ojw28 committed Nov 13, 2017
1 parent 5222494 commit 681a05d
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2017 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.google.android.exoplayer2.drm;

import android.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
* Utility methods for ClearKey.
*/
/* package */ final class ClearKeyUtil {

private static final String TAG = "ClearKeyUtil";
private static final Pattern REQUEST_KIDS_PATTERN = Pattern.compile("\"kids\":\\[\"(.*?)\"]");

private ClearKeyUtil() {}

/**
* Adjusts ClearKey request data obtained from the Android ClearKey CDM to be spec compliant.
*
* @param request The request data.
* @return The adjusted request data.
*/
public static byte[] adjustRequestData(byte[] request) {
if (Util.SDK_INT >= 27) {
return request;
}
// Prior to O-MR1 the ClearKey CDM encoded the values in the "kids" array using Base64 rather
// than Base64Url. See [Internal: b/64388098]. Any "/" characters that ended up in the request
// as a result were not escaped as "\/". We know the exact request format from the platform's
// InitDataParser.cpp, so we can use a regexp rather than parsing the JSON.
String requestString = Util.fromUtf8Bytes(request);
Matcher requestKidsMatcher = REQUEST_KIDS_PATTERN.matcher(requestString);
if (!requestKidsMatcher.find()) {
Log.e(TAG, "Failed to adjust request data: " + requestString);
return request;
}
int kidsStartIndex = requestKidsMatcher.start(1);
int kidsEndIndex = requestKidsMatcher.end(1);
StringBuilder adjustedRequestBuilder = new StringBuilder(requestString);
base64ToBase64Url(adjustedRequestBuilder, kidsStartIndex, kidsEndIndex);
return Util.getUtf8Bytes(adjustedRequestBuilder.toString());
}

/**
* Adjusts ClearKey response data to be suitable for providing to the Android ClearKey CDM.
*
* @param response The response data.
* @return The adjusted response data.
*/
public static byte[] adjustResponseData(byte[] response) {
if (Util.SDK_INT >= 27) {
return response;
}
// Prior to O-MR1 the ClearKey CDM expected Base64 encoding rather than Base64Url encoding for
// the "k" and "kid" strings. See [Internal: b/64388098].
try {
JSONObject responseJson = new JSONObject(Util.fromUtf8Bytes(response));
JSONArray keysArray = responseJson.getJSONArray("keys");
for (int i = 0; i < keysArray.length(); i++) {
JSONObject key = keysArray.getJSONObject(i);
key.put("k", base64UrlToBase64(key.getString("k")));
key.put("kid", base64UrlToBase64(key.getString("kid")));
}
return Util.getUtf8Bytes(responseJson.toString());
} catch (JSONException e) {
Log.e(TAG, "Failed to adjust response data: " + Util.fromUtf8Bytes(response), e);
return response;
}
}

private static void base64ToBase64Url(StringBuilder base64, int startIndex, int endIndex) {
for (int i = startIndex; i < endIndex; i++) {
switch (base64.charAt(i)) {
case '+':
base64.setCharAt(i, '-');
break;
case '/':
base64.setCharAt(i, '_');
break;
default:
break;
}
}
}

private static String base64UrlToBase64(String base64) {
return base64.replace('-', '+').replace('_', '/');
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,20 @@ private void postKeyRequest(int type, boolean allowRetry) {
try {
KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type,
optionalKeyRequestParameters);
if (C.CLEARKEY_UUID.equals(uuid)) {
final byte[] data = ClearKeyUtil.adjustRequestData(request.getData());
final String defaultUrl = request.getDefaultUrl();
request = new KeyRequest() {
@Override
public byte[] getData() {
return data;
}
@Override
public String getDefaultUrl() {
return defaultUrl;
}
};
}
postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget();
} catch (Exception e) {
onKeysError(e);
Expand All @@ -380,8 +394,12 @@ private void onKeyResponse(Object response) {
}

try {
byte[] responseData = (byte[]) response;
if (C.CLEARKEY_UUID.equals(uuid)) {
responseData = ClearKeyUtil.adjustResponseData(responseData);
}
if (mode == DefaultDrmSessionManager.MODE_RELEASE) {
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, responseData);
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
Expand All @@ -391,7 +409,7 @@ public void run() {
});
}
} else {
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData);
if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD
|| (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null))
&& keySetId != null && keySetId.length != 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import android.media.NotProvisionedException;
import android.media.UnsupportedSchemeException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
Expand Down Expand Up @@ -74,7 +75,7 @@ public void setOnEventListener(
final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() {
@Override
public void onEvent(@NonNull MediaDrm md, @NonNull byte[] sessionId, int event, int extra,
public void onEvent(@NonNull MediaDrm md, @Nullable byte[] sessionId, int event, int extra,
byte[] data) {
listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,16 @@ public static String normalizeLanguageCode(String language) {
return language == null ? null : new Locale(language).getLanguage();
}

/**
* Returns a new {@link String} constructed by decoding UTF-8 encoded bytes.
*
* @param bytes The UTF-8 encoded bytes to decode.
* @return The string.
*/
public static String fromUtf8Bytes(byte[] bytes) {
return new String(bytes, Charset.forName(C.UTF8_NAME));
}

/**
* Returns a new byte array containing the code points of a {@link String} encoded using UTF-8.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2017 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.google.android.exoplayer2.drm;

import static com.google.common.truth.Truth.assertThat;

import com.google.android.exoplayer2.C;
import java.nio.charset.Charset;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

/**
* Unit test for {@link ClearKeyUtil}.
*/
// TODO: When API level 27 is supported, add tests that check the adjust methods are no-ops.
@RunWith(RobolectricTestRunner.class)
public final class ClearKeyUtilTest {

@Config(sdk = 26, manifest = Config.NONE)
@Test
public void testAdjustResponseDataV26() {
byte[] data = ("{\"keys\":[{"
+ "\"k\":\"abc_def-\","
+ "\"kid\":\"ab_cde-f\"}],"
+ "\"type\":\"abc_def-"
+ "\"}").getBytes(Charset.forName(C.UTF8_NAME));
// We expect "-" and "_" to be replaced with "+" and "\/" (forward slashes need to be escaped in
// JSON respectively, for "k" and "kid" only.
byte[] expected = ("{\"keys\":[{"
+ "\"k\":\"abc\\/def+\","
+ "\"kid\":\"ab\\/cde+f\"}],"
+ "\"type\":\"abc_def-"
+ "\"}").getBytes(Charset.forName(C.UTF8_NAME));
assertThat(Arrays.equals(expected, ClearKeyUtil.adjustResponseData(data))).isTrue();
}

@Config(sdk = 26, manifest = Config.NONE)
@Test
public void testAdjustRequestDataV26() {
byte[] data = "{\"kids\":[\"abc+def/\",\"ab+cde/f\"],\"type\":\"abc+def/\"}"
.getBytes(Charset.forName(C.UTF8_NAME));
// We expect "+" and "/" to be replaced with "-" and "_" respectively, for "kids".
byte[] expected = "{\"kids\":[\"abc-def_\",\"ab-cde_f\"],\"type\":\"abc+def/\"}"
.getBytes(Charset.forName(C.UTF8_NAME));
assertThat(Arrays.equals(expected, ClearKeyUtil.adjustRequestData(data))).isTrue();
}

}

0 comments on commit 681a05d

Please sign in to comment.