FDPT: Full Decrypt Path Testing Application

Cherry pick of http://go/ag/9326830

This is a merge of the full decrypt path testing CLs from the Widevine
repo: http://go/wvgerrit/q/topic:FDPT-subsamples

This is the Full Decrypt Path Testing application that can be used by
device makers to verify that OEMCrypto is correctly decrypting content
to secure buffers.

Testing: Ran App.
Bug: 113594822

Change-Id: Icbb1e2f2e762bac3cc1b7b20749922c14ea24449
This commit is contained in:
Fred Gylys-Colwell
2019-10-28 14:57:22 -07:00
parent 2ee373e251
commit ea539673a4
78 changed files with 4565 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
# Android studio files:
local.properties
.gradle/
build/
captures/
gradle-app.setting
.externalNativeBuild
*.iml
vcs.xml
# Created by https://www.gitignore.io/api/intellij
# Edit at https://www.gitignore.io/?templates=intellij
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
.idea/sonarlint
# End of https://www.gitignore.io/api/intellij

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/FullDecryptPathTesting.iml" filepath="$PROJECT_DIR$/FullDecryptPathTesting.iml" />
<module fileurl="file://$PROJECT_DIR$/mobile/mobile.iml" filepath="$PROJECT_DIR$/mobile/mobile.iml" />
</modules>
</component>
</project>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@@ -0,0 +1,90 @@
# Full Decrypt Path Testing Application
Released August, 2019.
This is the Full Decrypt Path Testing application. The application is used
to test the full decrypt path of OEMCrypto.
The app computes a hash of a clear frame, encrypts the frame, and sends both to
OEMCrypto. OEMCrypto should decrypt, compute a hash, and then verify the
hash. The app will display any errors in the hash.
## Getting Started
If OEMCrypto on the device supports CRC32 hash, then the application should work
"out of the box". Install it using
adb install FullDecryptPathTesting.apk
To start a batch of tests, click on the "Start" button. The device needs to be
connected to WiFi to fetch a license. Once the license is installed, it should
cycle through a set of predefined tests and a bunch of random tests. The video
surface should display a single frame, which is the FDPT logo.
Press the "Clear" button to clear the logs if you want to run the tests again.
## Settings
To change which test to run, click on the "Setup" button. The default settings
run two of the four standard modes, "cenc" and "cbcs" with 5000 randomly
generated tests for each mode.
## Running on a production device
This application is intended to test decryption on a device running the full
Android stack. However, most Level 1 OEMCrypto implementations will not support
the decrypt hash feature on a production device. The feature will be optimized
out on production devices. For this reason, it should not be suprising if no
tests run when the settings for "Use Level 1" and "Use Secure Buffer" are set to
true.
## Running the application from adb
The settings filed are stored on the device in the shared preferences
directory. You can pull the XML file from the device, edit it, and push it back
to the device:
```shell
DIR=/data/data/com.google.widevine.fulldecryptpathtesting/shared_prefs
FILE=com.google.widevine.fulldecryptpathtesting_preferences.xml
adb pull $DIR/$FILE
```
Now edit the xml file on your host computer. You can then push the file back
to the directory. You might have to force-stop the application first, to force
the preferences to be read again.
```shell
adb shell am force-stop com.google.widevine.fulldecryptpathtesting
adb push $FILE $DIR/$FILE
```
Now that the settings have been updated, you can start the application from the
command line:
```shell
adb shell am start -n "com.google.widevine.fulldecryptpathtesting/.MainActivity" \
-d "start"
```
## Repeatable Tests
If you want to re-run a test with the exact same test cases, you may change the random seed
in the settings. Each run prints the random seed that it uses in the logcat.
If you don't set the seed, the application picks a random seed using a secure
random number generator.
## Source Code
The app builds with Android Studio.
Vendors who wish to supply their own hashing function instead of using CRC32
should edit the file HashGenerator.java or the C++ file native-lib.cpp.
BUILD ERRORS:
If you see the error below you might not have CMake installed.
```shell
>> * What went wrong:
>> A problem occurred configuring project ':mobile'.
>> > java.lang.NullPointerException (no error message)
```
Because the app includes native code written in C++, it requires CMake. From
the tools menu, select Android and Android SDK Manager. On the tab "SDK Tools",
make sure that Cmake is selected.

View File

@@ -0,0 +1,27 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,16 @@
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<boolean name="use_level_1" value="true" />
<boolean name="use_secure_buffer" value="true" />
<string name="codec">default</string>
<boolean name="test_cenc" value="true" />
<boolean name="test_cens" value="true" />
<boolean name="test_cbc1" value="false" />
<boolean name="test_cbcs" value="false" />
<string name="random_test_count">5000</string>
<boolean name="test_wrap" value="true" />
<boolean name="test_max" value="false" />
<boolean name="test_multi_subsample" value="false" />
<string name="random_seed"></string>
<boolean name="test_log_each_frame" value="false" />
</map>

View File

@@ -0,0 +1,17 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

View File

@@ -0,0 +1,6 @@
#Tue Jan 29 15:40:42 PST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip

View File

@@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

View File

@@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,46 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp
src/main/cpp/wvcrc.cpp
)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )

View File

@@ -0,0 +1,40 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.google.widevine.fulldecryptpathtesting"
minSdkVersion 24
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags "-std=c++14"
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:support-v4:26.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,96 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import static org.junit.Assert.*;
import android.app.Activity;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.SurfaceView;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test for the FrameGenerator.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class FullDecryptPathTest {
@Rule
public final ActivityTestRule<MainActivity> mActivityRule =
new ActivityTestRule<>(MainActivity.class);
@Test
public void initTest() throws Exception {
// Context of the app under test.
Context context = InstrumentationRegistry.getTargetContext();
assertEquals("com.google.widevine.fulldecryptpathtesting", context.getPackageName());
Logger logger = new Logger();
FrameGenerator frameGenerator = new FrameGenerator(context, logger);
frameGenerator.prepareKeyFrame();
assertTrue(0 < frameGenerator.getMinimumFrameSize());
}
@Test
public void sizeTest() throws Exception {
// Context of the app under test.
Context context = InstrumentationRegistry.getTargetContext();
assertEquals("com.google.widevine.fulldecryptpathtesting", context.getPackageName());
Logger logger = new Logger();
FrameGenerator frameGenerator = new FrameGenerator(context, logger);
frameGenerator.prepareKeyFrame();
assertTrue(0 < frameGenerator.getMinimumFrameSize());
int minSize = frameGenerator.getMinimumFrameSize();
for (int i = 0; i < 1000; i++) {
int size = minSize + i;
ITestFrameBuilder fb = new ByteArrayFrameBuilder(size);
byte[] frame = frameGenerator.buildtestFrame(fb);
assertEquals(size, frame.length);
}
// And one big one:
int size = 4 * 1024 * 1024;
ITestFrameBuilder fb = new ByteArrayFrameBuilder(size);
byte[] frame = frameGenerator.buildtestFrame(fb);
assertEquals(size, frame.length);
}
@Test
public void testAvcLevel1() throws InterruptedException {
testFullDecryptPath(CodecHandler.MIME_TYPE, true);
}
@Test
public void testAvcLevel3() throws InterruptedException {
testFullDecryptPath(CodecHandler.MIME_TYPE, false);
}
private void testFullDecryptPath(String mimeType, boolean useLevel1)
throws InterruptedException {
Logger logger = new Logger();
Activity activity = mActivityRule.getActivity();
SurfaceView surfaceView = (SurfaceView) activity.findViewById(R.id.playback_view);
TestParameters parameters = new TestParameters();
parameters.setUseLevel1(useLevel1);
parameters.setCodecName(MediaUtil.getCodecNameForMime(mimeType, useLevel1));
TestRunner runner = new TestRunner(logger, activity, surfaceView, parameters);
logger.setActivity(activity);
try {
IWorker worker = new InstrumentationTestWorker();
runner.doTest(worker);
} finally {
runner.cleanUp();
}
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
class InstrumentationTestWorker implements IWorker {
@Override
public void maybePause() {}
@Override
public boolean finishEarly() {
return false;
}
@Override
public void handleException(String message, Exception ex) {
throw new RuntimeException(message, ex);
}
@Override
public void handleError(String message) {
throw new RuntimeException(message);
}
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.widevine.fulldecryptpathtesting">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".FDPTApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
android:supportsRtl="true">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="MainActivity" />
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,34 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
#include <android/log.h>
#include <arpa/inet.h>
#include <jni.h>
#include <string>
#include "wvcrc32.h"
extern "C" JNIEXPORT void JNICALL
Java_com_google_widevine_fulldecryptpathtesting_HashGenerator_computeCRC(
JNIEnv *env, jobject /* this */, jbyteArray jframe, jbyteArray jhash) {
jbyte *frame = env->GetByteArrayElements(jframe, NULL);
jsize frame_size = env->GetArrayLength(jframe);
jbyte *hash = env->GetByteArrayElements(jhash, NULL);
jsize hash_size = env->GetArrayLength(jhash);
if (!frame || !hash) {
__android_log_write(ANDROID_LOG_ERROR, "FDPT_CRC",
"Null pointer passed to computeCRC");
} else if (hash_size != sizeof(uint32_t)) {
__android_log_write(ANDROID_LOG_ERROR, "FDPT_CRC",
"Hash size was not 4 bytes.");
} else {
uint32_t *hash_value = reinterpret_cast<uint32_t *>(hash);
uint32_t computed_hash = wvoec::wvcrc32n(reinterpret_cast<uint8_t *>(frame), frame_size);
*hash_value = htonl(computed_hash);
}
if (hash) env->ReleaseByteArrayElements(jhash, hash, 0);
if (frame) env->ReleaseByteArrayElements(jframe, frame, 0);
}

View File

@@ -0,0 +1,108 @@
// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
//
// Compute CRC32 Checksum. Needed for verification of WV Keybox.
//
#include "wvcrc32.h"
#include <arpa/inet.h>
namespace wvoec {
#define INIT_CRC32 0xffffffff
uint32_t wvrunningcrc32(const uint8_t* p_begin, int i_count, uint32_t i_crc) {
static uint32_t CRC32[256] = {
0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9,
0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005,
0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd,
0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9,
0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011,
0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd,
0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5,
0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81,
0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49,
0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95,
0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d,
0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae,
0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16,
0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca,
0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02,
0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066,
0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e,
0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692,
0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a,
0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e,
0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686,
0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a,
0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb,
0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f,
0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47,
0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b,
0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623,
0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7,
0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f,
0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3,
0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b,
0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f,
0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640,
0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c,
0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24,
0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30,
0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088,
0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654,
0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c,
0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18,
0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0,
0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c,
0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4
};
/* Calculate the CRC */
while (i_count > 0) {
i_crc = (i_crc << 8) ^ CRC32[(i_crc >> 24) ^ ((uint32_t) * p_begin)];
p_begin++;
i_count--;
}
return(i_crc);
}
uint32_t wvcrc32(const uint8_t* p_begin, int i_count) {
return(wvrunningcrc32(p_begin, i_count, INIT_CRC32));
}
uint32_t wvcrc32Init() {
return INIT_CRC32;
}
uint32_t wvcrc32Cont(const uint8_t* p_begin, int i_count, uint32_t prev_crc) {
return(wvrunningcrc32(p_begin, i_count, prev_crc));
}
uint32_t wvcrc32n(const uint8_t* p_begin, int i_count) {
return htonl(wvrunningcrc32(p_begin, i_count, INIT_CRC32));
}
} // namespace wvoec

View File

@@ -0,0 +1,23 @@
// Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
//
// Compute CRC32 Checksum. Needed for verification of WV Keybox.
//
#ifndef CDM_WVCRC32_H_
#define CDM_WVCRC32_H_
#include <stdint.h>
namespace wvoec {
uint32_t wvcrc32(const uint8_t* p_begin, int i_count);
uint32_t wvcrc32Init();
uint32_t wvcrc32Cont(const uint8_t* p_begin, int i_count, uint32_t prev_crc);
// Convert to network byte order
uint32_t wvcrc32n(const uint8_t* p_begin, int i_count);
} // namespace wvoec
#endif // CDM_WVCRC32_H_

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,48 @@
package com.google.widevine.fulldecryptpathtesting;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* byte[]-based test frame builder
*/
public class ByteArrayFrameBuilder implements ITestFrameBuilder {
private final int mSize;
private final ByteArrayOutputStream mBao;
public ByteArrayFrameBuilder(int size) {
mBao = new ByteArrayOutputStream(size);
mSize = size;
}
@Override
public int capacity() {
return mSize;
}
@Override
public void accept(byte[] bytes) {
try {
mBao.write(bytes);
} catch (IOException e) {
throw new RuntimeException("shouldn't happen", e);
}
}
@Override
public void accept(ByteBuffer bytes) {
throw new UnsupportedOperationException(bytes.toString());
}
@Override
public byte[] build() {
return mBao.toByteArray();
}
@Override
public ByteBuffer buildBuf() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,40 @@
package com.google.widevine.fulldecryptpathtesting;
import java.nio.ByteBuffer;
/**
* ByteBuffer-based test frame builder
*/
public class ByteBufferFrameBuffer implements ITestFrameBuilder {
private final ByteBuffer mBuf;
public ByteBufferFrameBuffer(ByteBuffer buf) {
this.mBuf = buf;
}
@Override
public int capacity() {
return mBuf.capacity();
}
@Override
public void accept(byte[] bytes) {
mBuf.put(bytes);
}
@Override
public void accept(ByteBuffer bytes) {
mBuf.put(bytes);
}
@Override
public byte[] build() {
return mBuf.hasArray() ? mBuf.array() : null;
}
@Override
public ByteBuffer buildBuf() {
return mBuf;
}
}

View File

@@ -0,0 +1,191 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceView;
import java.nio.ByteBuffer;
/** Creates a codec and deals with sending data down to the codec and the widevine plugin. */
public class CodecHandler {
private static final String TAG = "FDPT_CodecHandler";
private Logger mLogger;
private SurfaceView mView;
private MediaCrypto mCrypto;
private MediaCodec mCodec = null;
private ByteBuffer mInputBuffer;
private ByteBuffer mOutputBuffer;
private long mPresentationTime = -1;
private MediaFormat mCurrentFormat;
private final IWorker mWorker;
private static final int LONG_TIME_OUT_US = 2 * 1000 * 1000; // 2 second timeout.
private static final int SHORT_TIME_OUT_US = 2 * 1000; // 2 ms timeout.
private static final int FRAME_RATE = 25; // frames / second.
private static final int FRAME_DELTA = 1000 * 1000 / FRAME_RATE; // usec between frames.
public static final String MIME_TYPE = "video/avc";
CodecHandler(
Logger logger,
MediaCrypto crypto,
SurfaceView view,
MediaFormat format,
IWorker worker) {
mLogger = logger;
mCrypto = crypto;
mView = view;
mCurrentFormat = format;
mWorker = worker;
}
void createCodecByType(boolean mUseSecureBuffer) {
Log.d(TAG, "createCodecByType secure =" + mUseSecureBuffer);
try {
mCodec = MediaCodec.createDecoderByType(MIME_TYPE);
if (mUseSecureBuffer) {
String defaultName = mCodec.getName();
String secureName = defaultName + ".secure";
mCodec = MediaCodec.createByCodecName(secureName);
}
} catch (Exception e) {
mCodec = null;
mWorker.handleException("codec creation", e);
return;
}
mLogger.logInfo("Using codec " + mCodec.getName(), true);
}
void createCodecByName(String name) {
Log.d(TAG, "createCodecByName name =" + name);
try {
mCodec = MediaCodec.createByCodecName(name);
} catch (Exception e) {
mCodec = null;
mWorker.handleException("codec creation named " + name, e);
return;
}
mLogger.logInfo("Using codec " + mCodec.getName(), true);
}
boolean start() {
if (mCodec == null) return false;
if (mCrypto == null) return false;
Surface surface = mView.getHolder().getSurface();
Log.d(TAG, "using surface " + surface);
// Note: when tested, the format is width=1280, height=720,
// durationUs is set -- maybe we need to make it a larger number?
// XXX-- this is probably wrong. We want to add padding to that.
// max-input-size is set to 124410. We might want to increase this to be
// the maximum buffer size we test?
// frame-count = 411. maybe we should change this?
// mime = "video/avc", which agrees with mimeType.
// level = 512. what is this?
// frame-rate = 18. ok?
// TODO: get max size should be known by test case manager.
// mCurrentFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 200000);
try {
mCodec.configure(mCurrentFormat, surface, mCrypto, 0 /* decode */);
mCodec.start();
} catch (Exception ex) {
mWorker.handleException("starting codec.", ex);
return false;
}
return true;
}
boolean sendData(TestCase test, byte[] clearFrame, byte[] encryptedFrame, boolean lastFrame) {
try {
if (mCodec == null) return false;
int bufferIndex = mCodec.dequeueInputBuffer(LONG_TIME_OUT_US);
if (bufferIndex == -1) {
mLogger.setError("Could not get input buffer.");
return false;
}
mInputBuffer = mCodec.getInputBuffer(bufferIndex);
mInputBuffer.put(ByteBuffer.wrap(encryptedFrame));
int flags = 0;
// TODO: is this needed if we call flush in finishFrames below?
if (lastFrame) {
flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
}
// TODO: how heavily is this inforced? Should we get it from the clock?
long presentationTimeUs = FRAME_DELTA * test.getFrameNumber();
mCodec.queueSecureInputBuffer(
bufferIndex, 0 /* offset */, test.getCryptoInfo(), presentationTimeUs, flags);
processOutput();
} catch (Exception ex) {
mWorker.handleException("Codec issue.", ex);
return false;
}
return true;
}
private void processOutput() {
try {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (true) {
int outputBufferIndex = mCodec.dequeueOutputBuffer(info, SHORT_TIME_OUT_US);
if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
return;
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mCurrentFormat = mCodec.getOutputFormat();
return;
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
return;
} else if (outputBufferIndex >= 0) {
mCodec.releaseOutputBuffer(outputBufferIndex, true);
}
}
} catch (Exception ex) {
mWorker.handleException("process output.", ex);
}
}
void finishFrames() throws InterruptedException {
Log.d(TAG, "finish frames");
if (mCodec == null) return;
// TODO: clean this up. It should flush all the buffers, but it down't
// need to do much else.
Thread.sleep(2000); // TODO: probably not needed.
Log.d(TAG, "Process output in finishFrames.");
processOutput();
Log.d(TAG, "calling flush.");
mCodec.flush();
Log.d(TAG, "Another processOutput.");
processOutput();
cleanUp();
Log.d(TAG, "done with finish frames");
}
// This can be called multiple times.
void cleanUp() {
try {
Log.d(TAG, "cleanup");
if (mCodec == null) return;
Log.d(TAG, "codec stop.");
mCodec.stop();
Log.d(TAG, "codec release.");
mCodec.release();
mCodec = null;
} catch (Exception ex) {
mWorker.handleException("codec cleanup.", ex);
}
Log.d(TAG, "done with cleanup");
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.content.Context;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.preference.ListPreference;
import android.util.AttributeSet;
import android.util.Log;
import java.util.ArrayList;
/**
* This is a UI element on the settings/preference screen. It generates a list of codecs from which
* the user can choose.
*/
public class CodecPreference extends ListPreference {
private static final String TAG = "FDPT_CodecPreference";
public CodecPreference(Context context, AttributeSet attrs) {
super(context, attrs);
ArrayList<String> entries = new ArrayList<String>();
ArrayList<String> values = new ArrayList<String>();
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] codecInfos = list.getCodecInfos();
values.add(context.getString(R.string.default_codec));
entries.add(context.getString(R.string.default_codec_description));
for (MediaCodecInfo info : codecInfos) {
if (!info.isEncoder()) {
boolean include = false;
StringBuilder sb = new StringBuilder();
sb.append(info.getName() + ": ");
String[] supportedTypes = info.getSupportedTypes();
for (String string : supportedTypes) {
if (CodecHandler.MIME_TYPE.equals(string)) include = true;
sb.append(" " + string);
}
if (include) {
values.add(info.getName());
entries.add(sb.toString());
Log.d(TAG, sb.toString());
}
}
}
CharSequence[] array = new CharSequence[entries.size()];
setEntries(entries.toArray(array));
setEntryValues(values.toArray(array));
// TODO: pick best default based on secure or not secure,
// TODO: set value based on current preference value.
}
public CodecPreference(Context context) {
this(context, null);
}
}

View File

@@ -0,0 +1,404 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.media.MediaCodec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/** Encrypts frames based on the mode and pattern in the test case. */
public class Encryptor {
private static final String TAG = "FDPT_Encryptor";
private LicenseHolder mLicenseHolder;
private Logger mLogger;
static final int AES_BLOCK_SIZE = 16;
Encryptor(LicenseHolder licenseHolder, Logger logger) {
this.mLicenseHolder = licenseHolder;
this.mLogger = logger;
}
byte[] encryptFrame(byte[] frame, TestCase test) {
switch (test.getCryptoInfo().mode) {
default:
mLogger.setError("Unsupported crypto mode: " + test.getCryptoInfo().mode);
return frame;
case MediaCodec.CRYPTO_MODE_UNENCRYPTED:
return frame;
case MediaCodec.CRYPTO_MODE_AES_CTR:
if (test.getEncryptBlocks() > 0) {
return encryptCTRWithPattern(frame, test);
} else {
return encryptCTRNoSkipPattern(frame, test);
}
case MediaCodec.CRYPTO_MODE_AES_CBC:
if (test.getEncryptBlocks() > 0) {
return encryptCBCWithPattern(frame, test);
} else {
return encryptCBCNoSkipPattern(frame, test);
}
}
}
// Extract the encrypted subsamples into one single buffer. This is used for
// all but "cbcs" mode.
byte[] extractEncryptedSubsamples(final byte[] frame, TestCase test) {
MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
int numEncryptedBytes = 0;
int numSubSamples = cryptoInfo.numSubSamples;
for (int i = 0; i < numSubSamples; ++i) {
numEncryptedBytes += cryptoInfo.numBytesOfEncryptedData[i];
}
byte[] output = new byte[numEncryptedBytes];
int inputOffset = 0;
int outputOffset = 0;
for (int i = 0; i < numSubSamples; ++i) {
int numClear = cryptoInfo.numBytesOfClearData[i];
int numEncrypt = cryptoInfo.numBytesOfEncryptedData[i];
inputOffset += numClear;
System.arraycopy(frame, inputOffset, output, outputOffset, numEncrypt);
inputOffset += numEncrypt;
outputOffset += numEncrypt;
}
return output;
}
// In the function above, we pulled out the encrypted subsamples. In this function
// we re-assemble the subsamples after the encrypting the single buffer.
byte[] combineSubsamples(
final byte[] frame, // original
final byte[] encrypted,
TestCase test) {
MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
byte[] output = new byte[frame.length];
int numSubSamples = cryptoInfo.numSubSamples;
int frameOffset = 0;
int encryptOffset = 0;
for (int i = 0; i < numSubSamples; ++i) {
int numClear = cryptoInfo.numBytesOfClearData[i];
int numEncrypt = cryptoInfo.numBytesOfEncryptedData[i];
System.arraycopy(frame, frameOffset, output, frameOffset, numClear);
frameOffset += numClear;
System.arraycopy(encrypted, encryptOffset, output, frameOffset, numEncrypt);
frameOffset += numEncrypt;
encryptOffset += numEncrypt;
}
return output;
}
// This takes an IV and resets the low order bytes to 0. This is needed to manually wrap
// the iv on overflow.
byte[] resetIV(byte[] oldIV) {
byte[] iv = new byte[AES_BLOCK_SIZE];
for (int i = 0; i < 8; i++) {
iv[i] = oldIV[i];
}
for (int i = 8; i < 16; i++) {
iv[i] = 0;
}
return iv;
}
// 'CENC' protection scheme. This is counter mode with no pattern. We treat the encrypted
// subsamples as one long block in order to chain the iv.
byte[] encryptCTRNoSkipPattern(final byte[] frame, TestCase test) {
try {
byte[] tempBuffer = extractEncryptedSubsamples(frame, test);
byte[] encrypted = new byte[tempBuffer.length];
final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
SecretKey key = new SecretKeySpec(mLicenseHolder.getTestKey(), "AES");
MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
int bytesEncrypted = 0;
int bytesToEncrypt = encrypted.length;
int bytesToEncryptAfterWrap = 0;
// First, check to see if the iv will overflow/wrap when we increment it. For the CENC
// standard, we increment the low order 8 bytes as a 64 bit unsigned integer. The
// javax.crypto.Cipher will try to increment the iv as a 128 bit unsigned integer. To
// prevent this, we check to see if the IV would wrap, and break the buffer into two
// chunks if it would wrap. Convert the second batch of bytes to a 64 bit integer
// counter.
byte[] iv = cryptoInfo.iv;
long counter = 0;
for (int i = 8; i < 16; i++) {
counter = (counter << 8) + (iv[i] & 0xFFL);
}
// The 64 bit counter is a signed integer, because that's how Java works. If we pretend
// it is an unsigned integer, then addition overflows or wraps when the high order bit
// changes from 1 to 0. Since Java is doing signed addition, that means |counter| is
// negative, and |endCounter| is positive.
long endCounter = counter + (bytesToEncrypt / AES_BLOCK_SIZE);
if (counter < 0 && endCounter > 0) {
// The first batch to encrypt is -counter -- i.e. how far from the overflow point.
int firstBatch = (int) (-counter);
bytesToEncryptAfterWrap = bytesToEncrypt - firstBatch * AES_BLOCK_SIZE;
bytesToEncrypt = firstBatch * AES_BLOCK_SIZE;
}
// Loop at most twice to encrypt before the wrap and after the wrap.
while (bytesToEncrypt > 0) {
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
int result =
cipher.update(
tempBuffer,
bytesEncrypted,
bytesToEncrypt,
encrypted,
bytesEncrypted);
if (bytesToEncrypt != result) {
mLogger.setError(
"encryptCTRNoSkipPattern error: expected="
+ bytesToEncrypt
+ " , actual="
+ result);
}
cipher.doFinal();
// Increment counters. If there was a wrap, we have to loop back.
bytesEncrypted += bytesToEncrypt;
bytesToEncrypt = bytesToEncryptAfterWrap;
bytesToEncryptAfterWrap = 0; // Ensure we loop at most twice.
iv = resetIV(cryptoInfo.iv);
}
return combineSubsamples(frame, encrypted, test);
} catch (Exception ex) {
mLogger.logException("Encrypt CTR error: ", ex);
}
return frame;
}
// 'cbc1' protection scheme. This uses CBC with no pattern. We can combine all the
// subsamples and let the normal CBC algorithm chain the IVs. We may also assume that the
// encrypted subsamples end on an AES block boundary.
byte[] encryptCBCNoSkipPattern(final byte[] frame, TestCase test) {
try {
final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKey key = new SecretKeySpec(mLicenseHolder.getTestKey(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(test.getCryptoInfo().iv));
byte[] tempBuffer = extractEncryptedSubsamples(frame, test);
byte[] encrypted = new byte[tempBuffer.length];
int bytesToEncrypt = encrypted.length;
// If a sample does not end on a block boundary, we just copy the
// remainder as unencrypted bytes.
if (bytesToEncrypt % AES_BLOCK_SIZE != 0) {
int remainder = bytesToEncrypt % AES_BLOCK_SIZE;
bytesToEncrypt -= remainder;
System.arraycopy(tempBuffer, bytesToEncrypt, encrypted, bytesToEncrypt, remainder);
}
int result = cipher.update(tempBuffer, 0, bytesToEncrypt, encrypted, 0);
cipher.doFinal();
if (result != bytesToEncrypt) {
mLogger.setError(
"encryptCBCNoSkipPattern error: expected="
+ bytesToEncrypt
+ " , actual="
+ result);
}
return combineSubsamples(frame, encrypted, test);
} catch (Exception ex) {
mLogger.logException("Encrypt CBC error: ", ex);
}
return frame;
}
// 'cens' protection scheme. This is CTR with a pattern. All of the test cases that
// we need to support assume that the encrypted subsamples end on an AES block boundary.
byte[] encryptCTRWithPattern(final byte[] frame, TestCase test) {
try {
final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
SecretKey key = new SecretKeySpec(mLicenseHolder.getTestKey(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(test.getCryptoInfo().iv));
byte[] output = new byte[frame.length];
MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
int numSubSamples = cryptoInfo.numSubSamples;
// This is similar to the code above in encryptCTRNoSkipPattern(), except we increment
// the counter/iv after each pattern.
byte[] iv = cryptoInfo.iv;
long counter = 0;
for (int i = 8; i < 16; i++) {
counter = (counter << 8) + (iv[i] & 0xFFL);
}
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
int offset = 0;
for (int i = 0; i < numSubSamples; ++i) {
// clear bytes come first for ISO common encryption
int numClearBytes = cryptoInfo.numBytesOfClearData[i];
if (numClearBytes > 0) {
System.arraycopy(frame, offset, output, offset, numClearBytes);
offset += numClearBytes;
}
int numEncryptedBytes = cryptoInfo.numBytesOfEncryptedData[i];
if (numEncryptedBytes > 0) {
int bytesRemaining = numEncryptedBytes;
while (bytesRemaining > 0) {
int bytesToEncrypt = test.getEncryptBlocks() * AES_BLOCK_SIZE;
int bytesToSkip =
Math.min(
test.getSkipBlocks() * AES_BLOCK_SIZE,
bytesRemaining - bytesToEncrypt);
if (bytesRemaining < bytesToEncrypt) {
// The "cens" spec says that "any partial crypt_byte_block SHALL remain
// unencrypted." We are interpreting this to mean that if the data does
// not have enough bytes to fill all of the blocks in the encrypted part
// of the pattern, then none of the bytes are encrypted.
bytesToEncrypt = 0;
bytesToSkip = bytesRemaining;
}
// Check to see if the iv will wrap on this chunk. See the comments in
// encryptCTRNoSkipPattern() about counter overflow.
long newCounter = counter + (bytesToEncrypt / AES_BLOCK_SIZE);
int remainder = 0;
boolean overflow = false;
if (counter < 0 && newCounter >= 0) {
overflow = true;
int firstBatch = (int) (-counter);
remainder = bytesToEncrypt - firstBatch * AES_BLOCK_SIZE;
bytesToEncrypt = firstBatch * AES_BLOCK_SIZE;
}
int bytesEncrypted =
cipher.update(frame, offset, bytesToEncrypt, output, offset);
if (bytesEncrypted != bytesToEncrypt) {
mLogger.setError(
"encryptCTRWithPattern error: expected="
+ bytesToEncrypt
+ " , actual="
+ bytesEncrypted);
}
offset += bytesToEncrypt;
bytesRemaining -= bytesToEncrypt;
counter = newCounter;
if (overflow) {
cipher.doFinal();
iv = resetIV(cryptoInfo.iv);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
if (remainder > 0) {
int result =
cipher.update(frame, offset, remainder, output, offset);
if (result != remainder) {
mLogger.setError(
"encryptCTRWithPattern error: expected="
+ remainder
+ " , actual="
+ result);
}
offset += remainder;
bytesRemaining -= remainder;
}
}
System.arraycopy(frame, offset, output, offset, bytesToSkip);
offset += bytesToSkip;
bytesRemaining -= bytesToSkip;
}
}
}
if (offset != output.length) {
mLogger.setError("cens buffer size: " + output.length + " , actual=" + offset);
}
cipher.doFinal();
return output;
} catch (Exception ex) {
mLogger.logException("Encrypt CTR error: ", ex);
}
return frame;
}
// 'cbcs' protection scheme. This is used for HLS. This is CBC with a pattern.
// Unlike the other three schemes, the IV is reset at the start of each subsample.
// If the encrypted subsample does not end on an AES block boundary, we just copy the
// partial remaining block.
byte[] encryptCBCWithPattern(final byte[] frame, TestCase test) {
try {
final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKey key = new SecretKeySpec(mLicenseHolder.getTestKey(), "AES");
byte[] output = new byte[frame.length];
MediaCodec.CryptoInfo cryptoInfo = test.getCryptoInfo();
int numSubSamples = cryptoInfo.numSubSamples;
int offset = 0;
// If input length does not align on AES_BLOCK_SIZE,
// we encrypt up to the partial block and leave the partial
// block in the clear.
for (int i = 0; i < numSubSamples; ++i) {
// clear bytes come first for ISO common encryption
int numClearBytes = cryptoInfo.numBytesOfClearData[i];
if (numClearBytes > 0) {
System.arraycopy(frame, offset, output, offset, numClearBytes);
offset += numClearBytes;
}
// How many bytes left in this subsample...
int bytesRemaining = cryptoInfo.numBytesOfEncryptedData[i];
// Except we will handle a final partial block separately, because
// it is never encrypted.
int partialBlock = bytesRemaining % AES_BLOCK_SIZE;
// So, subtract the partial block.
bytesRemaining -= partialBlock;
if (bytesRemaining > 0) {
// resets the IV for each subsample
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(cryptoInfo.iv));
while (bytesRemaining > 0) {
int bytesToEncrypt = test.getEncryptBlocks() * AES_BLOCK_SIZE;
int bytesToSkip =
Math.min(
test.getSkipBlocks() * AES_BLOCK_SIZE,
bytesRemaining - bytesToEncrypt);
if (bytesRemaining < bytesToEncrypt) {
// The "cbcs" spec says that "any partial crypt_byte_block SHALL remain
// unencrypted." We are interpreting this to mean that if the data does
// not have enough bytes to fill all of the blocks in the encrypted part
// of the pattern, then none of the bytes are encrypted.
bytesToEncrypt = 0;
bytesToSkip = bytesRemaining;
}
if (bytesToEncrypt > 0) {
int bytesEncrypted =
cipher.update(frame, offset, bytesToEncrypt, output, offset);
if (bytesToEncrypt != bytesEncrypted) {
mLogger.setError(
"encryptCBCWithPattern error: expected="
+ bytesToEncrypt
+ " , actual="
+ bytesEncrypted);
}
offset += bytesEncrypted;
bytesRemaining -= bytesEncrypted;
}
if (bytesToSkip > 0) {
System.arraycopy(frame, offset, output, offset, bytesToSkip);
offset += bytesToSkip;
bytesRemaining -= bytesToSkip;
}
}
cipher.doFinal();
}
if (partialBlock > 0) {
// do not encrypt bytes that are less than block size
System.arraycopy(frame, offset, output, offset, partialBlock);
offset += partialBlock;
}
}
return output;
} catch (Exception ex) {
mLogger.logException("Encrypt CBC error: ", ex);
}
return frame;
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.app.Application;
public class FDPTApplication extends Application {
static final String TAG = "FDPT";
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
private Logger mLogger;
private Worker mWorker;
@Override
public void onCreate() {
super.onCreate();
mLogger = new Logger();
mWorker = new Worker(mLogger);
mWorker.start();
}
@Override
public void onTerminate() {
super.onTerminate();
mWorker.onStop();
}
@Override
public void onLowMemory() {
super.onLowMemory();
}
synchronized void setActivity(MainActivity activity) {
mWorker.onPause();
mLogger.setActivity(activity);
// TODO: tear down codec's surface and recreate it. This is only
// needed if we restart the activity while a test is already running.
}
Logger getLogger() {
return mLogger;
}
Worker getWorker() {
return mWorker;
}
}

View File

@@ -0,0 +1,184 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.support.annotation.RequiresApi;
import java.io.IOException;
import java.nio.ByteBuffer;
/** Generates a valid video key frame. */
public class FrameGenerator {
private static final String TAG = "FDPT_FrameGenerator";
private final int AES_BLOCK_SIZE = 16;
private final int NALU_4_BYTES_START_CODE = 0x0001;
private final int NALU_HEADER_SIZE = 5; // 4 bytes start code + 1 byte type
private final int SEI_MESSAGE_HEADER_SIZE = 2; // 1 byte type and 1 byte size
private final int MAX_SEI_PAYLOAD_SIZE = 255;
private final int MIN_HEXDUMP_BYTES = 32;
// smallest SEI message has 1 byte payload
private final int MIN_NALU_SIZE = NALU_HEADER_SIZE + SEI_MESSAGE_HEADER_SIZE + 1;
private Context mContext;
private Logger mLogger;
private MediaFormat mMediaFormat;
private String mMimeType;
private byte[] mKeyFrame;
private int mKeyFrameSize = 0;
private int getMediaFormatInteger(MediaFormat format, String key) {
return format.containsKey(key) ? format.getInteger(key) : 0;
}
FrameGenerator(Context context, Logger logger) {
this.mContext = context;
this.mLogger = logger;
}
@RequiresApi(api = Build.VERSION_CODES.P)
void prepareKeyFrame() {
MediaExtractor extractor = new MediaExtractor();
mKeyFrameSize = 0;
try (AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(R.raw.fdpt)) {
extractor.setDataSource(afd);
int tracks = extractor.getTrackCount();
for (int i = 0; i < tracks; ++i) {
MediaFormat mediaFormat = extractor.getTrackFormat(i);
String mimeType = mediaFormat.getString(MediaFormat.KEY_MIME);
if (mimeType.startsWith("video/")) {
mLogger.logInfo("track #" + i + " " + mediaFormat, true);
mLogger.logInfo(
"Width:"
+ getMediaFormatInteger(mediaFormat, MediaFormat.KEY_WIDTH)
+ " Height:"
+ getMediaFormatInteger(mediaFormat, MediaFormat.KEY_HEIGHT)
+ " "
+ mimeType,
true);
extractor.selectTrack(i);
}
}
do {
long sampleSize = extractor.getSampleSize();
if (sampleSize == -1) {
break;
} else if ((extractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
ByteBuffer inputBuffer = ByteBuffer.allocate((int) sampleSize);
mKeyFrameSize = extractor.readSampleData(inputBuffer, 0 /*offset*/);
mKeyFrame = new byte[mKeyFrameSize];
inputBuffer.get(mKeyFrame, 0, mKeyFrameSize);
mMediaFormat = extractor.getTrackFormat(extractor.getSampleTrackIndex());
mMimeType = mMediaFormat.getString(MediaFormat.KEY_MIME);
break;
}
} while (extractor.advance());
} catch (IOException ioex) {
mLogger.setError("Could not open video source, exception=" + ioex);
} finally {
extractor.release();
}
}
byte[] buildtestFrame(ITestFrameBuilder builder) {
return buildtestFrame(builder, builder.capacity());
}
// Appends one or more NALU (tail NAL units) at the end of the key frame.
// The NALU is of type 6: Supplemental enhancement information (SEI)
// and will be ignored by the decoder.
byte[] buildtestFrame(ITestFrameBuilder builder, int requestSize) {
final int SEI_MESSAGE_TYPE = 0x66;
final int UNREGISTERED_USER_DATA = 5;
final byte[] DEAD_FEED = new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xFE, (byte) 0xED};
int bytesToFill = requestSize - mKeyFrameSize;
int naluSize = MAX_SEI_PAYLOAD_SIZE + NALU_HEADER_SIZE + SEI_MESSAGE_HEADER_SIZE;
final int numNaluNeeded = bytesToFill / naluSize + (bytesToFill % naluSize == 0 ? 0 : 1);
int totalPayloadSize =
bytesToFill - (NALU_HEADER_SIZE + SEI_MESSAGE_HEADER_SIZE) * numNaluNeeded;
builder.accept(mKeyFrame);
for (int j = 0; j < numNaluNeeded; ++j) {
int payloadSize = Math.min(MAX_SEI_PAYLOAD_SIZE, totalPayloadSize);
naluSize = NALU_HEADER_SIZE + SEI_MESSAGE_HEADER_SIZE + payloadSize;
totalPayloadSize -= payloadSize;
// 0x66 (0110 0110) format:
// forbidden_zero_bit (1 bit) = 0
// nal_ref_idc (2 bits) = 3
// nal_unit_type (5 bits) = 6 (SEI message)
// SEI message {
// payload type (1 byte) 5 (user data unregistered)
// payload size (1 byte)
// actual message
// }
ByteBuffer nalu = ByteBuffer.allocate(naluSize);
nalu.putInt(NALU_4_BYTES_START_CODE);
nalu.put((byte) SEI_MESSAGE_TYPE);
nalu.put((byte) UNREGISTERED_USER_DATA);
nalu.put((byte) payloadSize);
for (int i = 0; i < payloadSize; ++i) {
nalu.put(DEAD_FEED[i % DEAD_FEED.length]);
}
builder.accept(nalu.array());
dumpTailNalu(naluSize, nalu, false /* verbose */);
}
return builder.build();
}
private void dumpTailNalu(int naluSize, ByteBuffer nalu, boolean verbose) {
if (verbose == false) return;
mLogger.logInfo("tail NALU size=" + naluSize, true);
mLogger.logInfo(
"tail NALU:\n"
+ HexUtil.dumpPrettyHexBytes(
nalu.array(), 0, Math.min(MIN_HEXDUMP_BYTES, naluSize)),
true);
}
private void dumpPartialFrame(byte[] testFrame, int naluSize, boolean verbose) {
if (verbose == false) return;
mLogger.logInfo(
"test frame head:\n" + HexUtil.dumpPrettyHexBytes(testFrame, 0, MIN_HEXDUMP_BYTES),
true);
mLogger.logInfo(
"test frame tail:\n"
+ HexUtil.dumpPrettyHexBytes(
testFrame,
testFrame.length - naluSize,
Math.min(MIN_HEXDUMP_BYTES, naluSize)),
true);
}
// TODO: is this needed?
final String getMimeType() {
return mMimeType;
}
// The minimum frame size is keyFrame plus a smallest NALU for padding.
final int getMinimumFrameSize() {
return mKeyFrame.length + MIN_NALU_SIZE;
}
final MediaFormat getMediaFormat() {
return mMediaFormat;
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
/**
* Computes a hash of each frame. This version of HashGenerator uses native C++ code to compute a
* CRC of the frame. Vendors who wish to use a different type of hash or checksum should modify this
* class. You can either write your code in Java, or look in the directory mobile/src/main/cpp and
* modify the native-lib.cpp file. Make sure you update mobile/CMakeLists.txt if you add any new C++
* source files.
*/
public class HashGenerator {
HashGenerator() {}
// Compute the hash of one frame.
byte[] getHash(byte[] frame, TestCase test) {
byte[] hash = new byte[4]; // A CRC 32 is 4 bytes long.
computeCRC(frame, hash);
return hash;
}
public native void computeCRC(byte[] frame, byte[] hash);
}

View File

@@ -0,0 +1,69 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
/**
* Dump buffer in hex, 16 bytes per row.
*
* @param inputBuffer buffer to dump
* @param offset index to the first byte to dump
* @param length length of buffer
*/
final class HexUtil {
static String dumpPrettyHexBytes(byte[] inputBuffer, int offset, int length) {
final int NUM_COLUMNS = 16;
StringBuilder builder = new StringBuilder();
for (int row = offset; row < offset + length; row += NUM_COLUMNS) {
builder.append(String.format("%06d: ", row));
for (int col = 0; col < NUM_COLUMNS; ++col) {
if (row + col < inputBuffer.length) {
builder.append(String.format("%02x ", inputBuffer[row + col]));
} else {
break;
}
}
builder.append(String.format("\n"));
}
return builder.toString();
}
static String dumpHexBytes(byte[] inputBuffer, int offset, int length) {
StringBuilder builder = new StringBuilder();
for (int i = offset; i < length; i++) {
builder.append(String.format("%02x", inputBuffer[i]));
}
return builder.toString();
}
private static byte charsToByte(char left, char right) {
final int leftByte = Character.digit(left, 16) << 4;
final int rightByte = Character.digit(right, 16);
return (byte) ((leftByte | rightByte) & 0xFF);
}
public static byte[] fromString(String s) {
final int length = s.length();
final byte[] data = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
data[i / 2] = charsToByte(s.charAt(i), s.charAt(i + 1));
}
return data;
}
public static String toString(byte[] array) {
StringBuilder sb = new StringBuilder();
for (byte b : array) {
sb.append(String.format("%02X", b));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,15 @@
package com.google.widevine.fulldecryptpathtesting;
import java.nio.ByteBuffer;
/**
* Interface to build test frame from a key frame and trailing nal units
*/
public interface ITestFrameBuilder {
int capacity();
void accept(byte[] bytes);
void accept(ByteBuffer bytes);
byte[] build();
ByteBuffer buildBuf();
}

View File

@@ -0,0 +1,15 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
public interface IWorker {
void maybePause();
boolean finishEarly();
void handleException(String message, Exception e);
void handleError(String message);
}

View File

@@ -0,0 +1,335 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.content.Context;
import android.media.DeniedByServerException;
import android.media.MediaCrypto;
import android.media.MediaCryptoException;
import android.media.MediaDrm;
import android.media.NotProvisionedException;
import android.media.ResourceBusyException;
import android.media.UnsupportedSchemeException;
import android.net.ConnectivityManager;
import android.net.Network;
import java.io.IOException;
import java.util.Arrays;
import java.util.UUID;
/** Generates a widevine license. If the device needs to be provisioned, it also does that. */
public class LicenseHolder {
private static final String TAG = "FDPT_LicenseHolder";
private Context mContext;
private Logger mLogger;
private boolean mUseLevel1;
private static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
private static final String ORIGIN_KEY = "origin";
private static final String ORIGIN = "full-decrypt-path-testing";
private static final String mServerURL = "https://proxy.uat.widevine.com/proxy";
private static final String VIDEO_ID = "video_id=FDPT-test-contnt";
// A PSSH generated by the integration console, using the key id and content
// id below. This content id is valid on UAT and Widevine staging servers.
private final byte[] FDPT_PSSH =
HexUtil.fromString("2210464450542d746573742d636f6e746e7448e3dc959b06");
// Key ID for test content.
static final byte[] TEST_KEY_ID = "FDPT-test-key-id".getBytes();
// == 464450542d746573742d6b65792d694 in hex.
// The content id. Used by the license server.
private static final byte[] TEST_CONTENT_ID = "FDPT-test-contnt".getBytes();
// == 464450542d746573742d636f6e746e74 in hex.
// The key that is used to decrypt content.
private static final byte[] TEST_KEY_DATA = "0123456789abcdef".getBytes();
// == 30313233343536373839616263646566 in hex.
private MediaDrm mDrm = null;
private byte[] mSession;
private byte[] mKeySet;
private boolean mHasLicense = false;
LicenseHolder(Logger logger, boolean useLevel1, Context context) {
this.mContext = context;
this.mLogger = logger;
this.mUseLevel1 = useLevel1;
}
MediaCrypto getMediaCrypto() {
try {
if (!mHasLicense) return null;
return new MediaCrypto(WIDEVINE_UUID, mSession);
} catch (MediaCryptoException ex) {
mLogger.logException("Fetching MediaCrypto", ex);
return null;
}
}
void cleanUp() {
if (mDrm != null) {
try {
if (mSession != null) {
mDrm.closeSession(mSession);
mSession = null;
}
mDrm.close();
mDrm = null;
} catch (Exception ex) {
mLogger.logException("License cleanup", ex);
}
}
}
void init() {
try {
mDrm = new MediaDrm(WIDEVINE_UUID);
} catch (UnsupportedSchemeException ex) {
mLogger.logInfo("Widevine not supported: " + ex, true);
mLogger.setError("Widevine unsupported.");
return;
}
mDrm.setPropertyString(ORIGIN_KEY, ORIGIN);
if (!mUseLevel1) mDrm.setPropertyString("securityLevel", "L3");
dumpInfo();
}
boolean shouldSkipTest() {
if (mDrm == null) {
init();
if (mDrm == null) return true;
}
int oemCryptoVersion = -1;
String version = mDrm.getPropertyString("oemCryptoApiVersion");
try {
oemCryptoVersion = Integer.parseInt(version);
} catch (NumberFormatException e) {
mLogger.logException("OEMCrypto Format " + version, e);
return true;
}
if (oemCryptoVersion < 15) {
final CharSequence wrongApi =
"OEMCrypto v15 or greater is required,\n"
+ "the device is reporting v"
+ oemCryptoVersion
+ ".\nSkip all tests.";
mLogger.setError(wrongApi.toString());
return true;
}
final String hashSupport = mDrm.getPropertyString("decryptHashSupport");
if (!"1".equals(hashSupport)) { // OEMCrypto_CRC_Clear_Buffer
final CharSequence noHash =
"Device returns OEMCrypto_Hash_Not_Supported.\n" + "Skip all tests.";
mLogger.setError(noHash.toString());
return true;
}
return false;
}
private void dumpInfo() {
mLogger.logInfo(android.os.Build.BRAND + " " + android.os.Build.BOARD, true);
dumpString(R.string.key_security_level, R.string.security_level);
dumpString(R.string.key_system_id, R.string.system_id);
dumpString(R.string.key_cdm_version, R.string.cdm_version);
dumpString(R.string.key_oemcrypto_api, R.string.oemcrypto_api);
dumpString(R.string.key_oemcrypto_build_info, R.string.oemcrypto_build_info);
dumpString(R.string.key_hash_support, R.string.hash_support);
dumpString(R.string.key_resource_rating, R.string.resource_rating);
}
private void dumpString(int key_index, int name_index) {
String name = mContext.getString(name_index);
String key = mContext.getString(key_index);
try {
mLogger.logInfo(name + ": " + mDrm.getPropertyString(key), true);
} catch (Exception ex) {
mLogger.logInfo(name + ": not defined", true);
}
}
int getResourceRatingTier() {
String key = mContext.getString(R.string.key_resource_rating);
try {
String value = mDrm.getPropertyString(key);
return Integer.parseInt(value);
} catch (Exception ex) {
mLogger.logInfo(key + ": property not available", true);
return 0;
}
}
// Fetch and install the license. If provisioning is needed, provision and
// then try to fetch and install again.
void getLicense() {
if (!checkInternet()) return;
try {
if (mDrm == null) {
init();
}
mSession = mDrm.openSession();
mLogger.setStatus("Generating license request");
byte[] initData = FDPT_PSSH;
String mimeType = "cenc";
String url = mServerURL + "?" + VIDEO_ID;
final MediaDrm.KeyRequest request =
mDrm.getKeyRequest(
mSession, initData, mimeType, MediaDrm.KEY_TYPE_STREAMING, null);
if (request == null) {
mLogger.setError("Key request not generated.");
return;
}
mLogger.setStatus("Requesting license");
Post post = new Post(url, request.getData());
Post.Response response = post.send();
byte[] data = parseResponseBody(response.body);
mKeySet = mDrm.provideKeyResponse(mSession, data);
if (mKeySet == null) {
mLogger.setError("Empty key set.");
return;
}
mLogger.setStatus("License installed");
mHasLicense = true;
} catch (NotProvisionedException e) {
if (fetchProvisioning()) {
// Once provisioned, start over.
getLicense();
}
} catch (ResourceBusyException e) {
mLogger.logInfo("Could not open session: " + e, true);
mLogger.setError("Failed to open session");
return;
} catch (DeniedByServerException e) {
mLogger.logInfo("License Denied: " + e, true);
mLogger.setError("License denied");
return;
} catch (SecurityException e) {
// This happens if the app does not have permission to connect to the
// internet.
mLogger.logInfo("Permission failure: " + e, true);
mLogger.setError("security exception: no internet permission?");
return;
} catch (IOException e) {
mLogger.logInfo("Could not get license: " + e, true);
mLogger.setError("Failed to get license");
return;
}
}
private byte[] parseResponseBody(byte[] responseBody) throws IOException {
final String bodyString = new String(responseBody, "UTF-8");
if (!bodyString.startsWith("GLS/")) {
return responseBody;
}
if (!bodyString.startsWith("GLS/1.")) {
throw new IOException("Invalid server version, expected 1.x");
}
final int drmMessageOffset = bodyString.indexOf("\r\n\r\n");
if (drmMessageOffset == -1) {
throw new IOException("Invalid server response, could not locate drm message");
}
return Arrays.copyOfRange(responseBody, drmMessageOffset + 4, responseBody.length);
}
private boolean fetchProvisioning() {
try {
mLogger.setStatus("Fetching provisioning");
final MediaDrm.ProvisionRequest request = mDrm.getProvisionRequest();
final byte[] data = request.getData();
final String signedUrl =
String.format("%s&signedRequest=%s", request.getDefaultUrl(), new String(data));
final Post post = new Post(signedUrl, null);
final Post.Response response = post.send();
mDrm.provideProvisionResponse(response.body);
mLogger.setStatus("Finished provisioning");
return true;
} catch (SecurityException e) {
// This happens if the app does not have permission to connect to the
// internet.
mLogger.logInfo("Permission failure: " + e, true);
mLogger.setError("security exception: no internet permission?");
return false;
} catch (Exception ex) {
mLogger.logInfo("Could not provision: " + ex, true);
mLogger.setError("failed to provision");
return false;
}
}
boolean hasLicense() {
return mHasLicense;
}
private boolean checkInternet() {
ConnectivityManager connectivityManager =
(ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
Network network = connectivityManager.getActiveNetwork();
if (network == null) {
mLogger.setError("No network connected.");
return false;
}
return true;
}
boolean sendHash(byte[] hash, TestCase test, IWorker worker) {
if (mDrm == null || mSession == null) {
mLogger.setError("Session not open");
return false;
}
try {
String sSession = new String(mSession);
String value =
sSession
+ ","
+ test.getFrameNumber()
+ ","
+ HexUtil.dumpHexBytes(hash, 0, hash.length);
mDrm.setPropertyString("decryptHashSessionId", sSession);
mDrm.setPropertyString("decryptHash", value);
} catch (Exception ex) {
worker.handleException("setting hash", ex);
return false;
}
return true;
}
boolean foundError(TestCaseFactory factory, IWorker worker) {
try {
String result = mDrm.getPropertyString("decryptHashError");
String[] csv = result.split(",");
if (csv.length != 2) {
mLogger.setError("Confusing hash error: " + result);
return true;
}
String code = csv[0];
if (code.equals("0")) { // OEMCrypto_SUCCESS.
return false;
} else if (code.equals("53")) { // OEMCrypto_BAD_HASH
int frameNumber = Integer.parseInt(csv[1]);
int index = frameNumber - 1;
TestCase test = factory.getTest(index);
mLogger.setError(test.getDescription() + " hash wrong");
test.logInfo(mLogger);
return true;
} else {
mLogger.setError("Confusing hash code: " + result);
}
} catch (Exception ex) {
worker.handleException("checking hash error", ex);
return true;
}
return false;
}
final byte[] getTestKey() {
return TEST_KEY_DATA;
}
}

View File

@@ -0,0 +1,145 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.app.Activity;
import android.text.Html;
import android.util.Log;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Calendar;
/**
* Logs information to logcat and also important information and errors to a status view in the UI.
*/
class Logger {
private static final String TAG = "FDPT";
// The activity to which we post UI tasks.
private Activity mActivity;
// The text view used for logging.
private TextView mLogView;
private TextView mStatusView;
private StringBuffer mLogBuilder;
private boolean mNewLogLine = true;
private String mStatusString = null;
private String mErrorString = null;
private int mFrame = -1;
private int mMaximumFrame = 0;
Logger() {
mLogBuilder = new StringBuffer();
}
void setActivity(Activity activity) {
this.mActivity = activity;
this.mLogView = (TextView) mActivity.findViewById(R.id.log_output);
this.mStatusView = (TextView) mActivity.findViewById(R.id.status);
}
// Only call from UI thread.
void clearLog() {
mLogBuilder = new StringBuffer();
if (mLogView != null) mLogView.setText("");
}
// Log information to the log view and to logcat.
void logInfo(final String s, final boolean end_of_line) {
Log.i(TAG, s);
if (mActivity != null) {
mActivity.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mNewLogLine) {
String time_stamp =
new SimpleDateFormat("MM-dd HH:mm:ss ")
.format(Calendar.getInstance().getTime());
mLogBuilder.append(time_stamp);
}
mLogBuilder.append(s);
if (end_of_line) {
mLogBuilder.append("<br>");
mNewLogLine = true;
} else {
mLogBuilder.append(", ");
mNewLogLine = false;
}
mLogView.setText(Html.fromHtml(mLogBuilder.toString()));
}
});
}
}
void logException(final String s, Exception ex) {
setError(s + " " + ex.getMessage());
Log.e(TAG, s, ex);
}
// Log information to the status view, the log view, and to logcat.
// Run on the worker thread.
synchronized void setStatus(String status) {
mStatusString = status;
logInfo("Status Changed: " + mStatusString, true);
updateStatus();
}
// Update the current frame number in the counter view.
// Run on the worker thread.
synchronized void setStatus(int currentFrame, int maximumFrame) {
mFrame = currentFrame;
mMaximumFrame = maximumFrame;
Log.i(TAG, "Frame " + (currentFrame + 1) + "/" + maximumFrame);
updateStatus();
}
// Log information to the status view, the log view, and to logcat. An error
// will not be replaced by a new status -- only by new errors.
// Run on the worker thread.
synchronized void setError(String errorString) {
if (mErrorString == null) mErrorString = errorString;
logInfo("Error: " + mErrorString, true);
updateStatus();
}
synchronized String getErrorString() {
return mErrorString;
}
// This called by the worker to indicate a new test is starting.
synchronized void clearStatus() {
mFrame = -1;
mMaximumFrame = 0;
mErrorString = null;
mStatusString = "<none>";
updateStatus();
}
synchronized void updateStatus() {
// TODO: don't flood the ui thread with frame number updates.
String status;
if (mErrorString != null) {
status = "<font color='red'>" + mErrorString + "</font>";
} else {
status = mStatusString;
}
if (mMaximumFrame > 0) {
// It looks better to start counting at 1.
status += " Frame " + (mFrame + 1) + "/" + mMaximumFrame + ".";
}
final String newStatus = status;
if (mActivity != null) {
mActivity.runOnUiThread(
new Runnable() {
@Override
public void run() {
mStatusView.setText(Html.fromHtml(newStatus));
}
});
}
}
}

View File

@@ -0,0 +1,121 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.SurfaceView;
import android.view.View;
/** This is the main view for the application on phones and tablets. */
public class MainActivity extends Activity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
private Logger mLogger;
private Worker mWorker;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle(R.string.short_app_name);
FDPTApplication app = (FDPTApplication) getApplicationContext();
app.setActivity(this);
mLogger = app.getLogger();
mWorker = app.getWorker();
// Note: If you are starting things in batch mode, you might also want to edit
// preferences in batch mode. Use this:
// DIR=/data/data/com.google.widevine.fulldecryptpathtesting/shared_prefs
// FILE=com.google.widevine.fulldecryptpathtesting_preferences.xml
// adb pull $DIR/$FILE
// Then edit $FILE and then
// adb push $FILE $DIR/$FILE
Intent intent = getIntent();
String data = intent.getDataString();
if ("start".equals(intent.getDataString())) {
// For batch mode, use this command to start the tests:
// adb shell am start -n "com.google.widevine.fulldecryptpathtesting/.MainActivity" \
// -d "start"
startTest(null);
}
if ("stop".equals(intent.getDataString())) {
// For batch mode, use this command to stop the tests:
// adb shell am start -n "com.google.widevine.fulldecryptpathtesting/.MainActivity" \
// -d "stop"
stopTest(null);
}
}
@Override
protected void onPause() {
super.onPause();
mWorker.onPause();
}
@Override
protected void onResume() {
super.onResume();
mWorker.onResume();
}
@Override
protected void onDestroy() {
super.onStop();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.action_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_setup) {
configureTest(null);
}
if (item.getItemId() == R.id.menu_play) {
startTest(null);
}
return true;
}
// Called when Setup button is pressed.
public void configureTest(View view) {
startActivity(new Intent(this, SettingsActivity.class));
}
// Called when start test button is pressed.
public void startTest(View view) {
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.playback_view);
Context context = getBaseContext();
TestParameters parameters = new TestParameters(context, mLogger);
TestRunner runner = new TestRunner(mLogger, context, surfaceView, parameters);
mWorker.startTest(runner);
}
// Called when stop test button is pressed.
public void stopTest(View view) {
mWorker.cancelTest();
}
// Called when clear button is pressed.
public void clearLog(View view) {
mLogger.clearLog();
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecList;
import java.util.Arrays;
import java.util.List;
public final class MediaUtil {
private MediaUtil() {}
public static String getCodecNameForMime(String mime, boolean secure) {
MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS);
mcl.getCodecInfos();
for (MediaCodecInfo info : mcl.getCodecInfos()) {
if (info.isEncoder()) {
continue;
}
List<String> supportedTypes = Arrays.asList(info.getSupportedTypes());
if (!supportedTypes.contains(mime)) {
continue;
}
if (secure) {
CodecCapabilities caps = info.getCapabilitiesForType(mime);
if (caps.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback)) {
return info.getName();
}
} else {
return info.getName();
}
}
return null;
}
}

View File

@@ -0,0 +1,189 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.util.Base64;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/** Post a request to a license or provisioning server and wait for the response. */
public final class Post {
private static final int TIMEOUT_MS = 5000;
private static final int MAX_TRIES = 5;
private static final String TAG = "WVPostRequest";
static final class Response {
final int code;
final byte[] body;
Response(int code, byte[] body) {
this.code = code;
this.body = body;
}
}
private static final byte[] EMPTY_BODY = new byte[0];
private final String mUrl;
private final byte[] mData;
private final boolean mExpectOutput;
private final Map<String, String> mProperties = new HashMap<>();
Post(String url, byte[] data) {
mUrl = url;
mData = data == null ? EMPTY_BODY : Arrays.copyOf(data, data.length);
mExpectOutput = data != null;
setProperty("Accept", "*/*");
setProperty("User-Agent", "Widevine Full Decrypt Path Test Application");
setProperty("Connection", "close");
if (data != null) setProperty("Content-Type", "application/json");
}
void setProperty(String key, String value) {
mProperties.put(key, value);
}
Response send() throws IOException {
int tries = 1;
boolean needRetry = true;
Response response = null;
while (needRetry) {
HttpURLConnection connection = null;
needRetry = false;
try {
connection = (HttpURLConnection) new URL(mUrl).openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(mExpectOutput);
connection.setDoInput(true);
connection.setConnectTimeout(TIMEOUT_MS);
connection.setReadTimeout(TIMEOUT_MS);
for (final Map.Entry<String, String> property : mProperties.entrySet()) {
connection.setRequestProperty(property.getKey(), property.getValue());
}
try (final OutputStream out = connection.getOutputStream()) {
out.write(mData);
}
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int connectionResponse = connection.getResponseCode();
if (connectionResponse < 400) {
try (final InputStream inputStream = connection.getInputStream()) {
connectStreams(inputStream, outputStream);
}
} else {
try (final InputStream inputStream = connection.getErrorStream()) {
connectStreams(inputStream, outputStream);
}
}
response = new Response(connection.getResponseCode(), outputStream.toByteArray());
// Logging license request / responses
// Catching in try/catch in case other type of request / response that isn't
// loggable
// Provisioning has an empty request and we have no way to decode the
// response at present, so removing it from request / response logging
try {
if (!mUrl.contains("provisioning")) {
String myRequest = Base64.encodeToString(mData, Base64.NO_WRAP);
Log.i("LICENSE_REQUEST:", myRequest);
Log.i("FLUSH", "flushing");
}
} catch (Exception ex) {
Log.e(
"LICENSE_REQUEST",
"Failure to log licensing request in videoplayer Post.",
ex);
}
try {
if (!mUrl.contains("provisioning")) {
String myResponse =
Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP);
myResponse = myResponse.equals("") ? "EMPTY" : myResponse;
if (connectionResponse >= 400) {
String s = new String(outputStream.toByteArray(), "UTF-8");
Log.i("LICENSE_RESPONSE", s);
} else {
Log.i("LICENSE_RESPONSE", myResponse);
}
Log.i("FLUSH", "flushing");
}
} catch (Exception ex) {
Log.e(
"LICENSE_RESPONSE",
"Failure to log licensing response in videoplayer Post.",
ex);
}
} catch (SocketTimeoutException ste) {
if (tries == MAX_TRIES) {
throw ste;
}
Log.w(TAG, "Retrying after receiving SocketTimeoutException on try " + tries);
tries++;
needRetry = true;
} catch (Exception ex) {
Log.e(TAG, "Unexpected failure in response / request.", ex);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
if (response == null) {
throw new IOException("Empty response");
}
if (response.code != 200) {
throw new IOException("Server returned HTTP error code " + response.code);
}
if (response.body == null) {
throw new IOException("No response from server");
}
if (response.body.length == 0) {
throw new IOException("Empty response from server");
}
return response;
}
private static void connectStreams(InputStream in, OutputStream out) throws IOException {
final byte scratch[] = new byte[1024];
int read; /* declare this here so that the for loop can be aligned */
for (read = in.read(scratch, 0, scratch.length);
read != -1;
read = in.read(scratch, 0, scratch.length)) {
out.write(scratch, 0, read);
}
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
// This is the UI for the settings or preferences.
// TODO: when the user changes L1/L3, pick a different default codec.
// TODO: pick a good default codec.
public class SettingsActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getFragmentManager()
.beginTransaction()
.replace(android.R.id.content, new MySettingsFragment())
.commit();
}
public static class MySettingsFragment extends PreferenceFragment {
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Read the list of preferences that the user can change from the file
// mobile/src/main/res/xml/preferences.xml.
addPreferencesFromResource(R.xml.preferences);
}
}
}

View File

@@ -0,0 +1,96 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.media.MediaCodec;
// A single test case.
class TestCase {
private String mDescription;
// The size of the frame buffer.
private int mBufferSize;
// The frame number. Used for documenting problems.
private int mFrameNumber;
private MediaCodec.CryptoInfo.Pattern mPattern;
// The encryption pattern and mode.
private MediaCodec.CryptoInfo mCryptoInfo;
TestCase(
int frameNumber,
String description,
int bufferSize,
MediaCodec.CryptoInfo cryptoInfo,
MediaCodec.CryptoInfo.Pattern pattern) {
this.mDescription = description;
this.mBufferSize = bufferSize;
this.mPattern = pattern;
this.mCryptoInfo = cryptoInfo;
this.mFrameNumber = frameNumber;
}
public String toString() {
return "Frame: "
+ mFrameNumber
+ ", Buffer size: "
+ mBufferSize
+ ", Pattern = "
+ mPattern
+ ", Crypto: "
+ mCryptoInfo;
}
public void logInfo(Logger logger) {
logger.logInfo("Frame " + mFrameNumber + ": " + mDescription, true);
StringBuilder sizes = new StringBuilder("size = [");
for (int i = 0; i < mCryptoInfo.numSubSamples; i++) {
sizes.append(" (");
sizes.append(mCryptoInfo.numBytesOfClearData[i]);
sizes.append(", ");
sizes.append(mCryptoInfo.numBytesOfEncryptedData[i]);
sizes.append(");");
}
sizes.append("] total=");
sizes.append(mBufferSize);
logger.logInfo(sizes.toString(), true);
String mode =
(mCryptoInfo.mode == MediaCodec.CRYPTO_MODE_AES_CTR) ? "Mode = CTR" : "Mode = CBC";
logger.logInfo(
mode
+ ", Pattern = (e="
+ mPattern.getEncryptBlocks()
+ ", s="
+ mPattern.getSkipBlocks()
+ ")",
true);
}
final MediaCodec.CryptoInfo getCryptoInfo() {
return mCryptoInfo;
}
final int getFrameNumber() {
return mFrameNumber;
}
final int getBufferSize() {
return mBufferSize;
}
final int getEncryptBlocks() {
return mPattern.getEncryptBlocks();
}
final int getSkipBlocks() {
return mPattern.getSkipBlocks();
}
final String getDescription() {
return mDescription;
}
}

View File

@@ -0,0 +1,404 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.media.MediaCodec;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Random;
// Generates a list of test cases. It uses preferences to pick the test cases.
public class TestCaseFactory {
private static final String TAG = "FDPT_TestCaseFactory";
private Logger mLogger;
private Random mRandom;
private ArrayList<TestCase> mTests = new ArrayList<TestCase>();
private int mMinSize;
private TestParameters mParameters;
private int mRRTier;
static final int AES_BLOCK_SIZE = 16;
TestCaseFactory(Logger logger, int minSize, int resourceRatingTier, TestParameters parameters) {
this.mLogger = logger;
this.mMinSize = minSize;
this.mParameters = parameters;
this.mRRTier = resourceRatingTier;
if (minSize <= 0) {
mLogger.setError("Frame Generator failed.");
return;
}
int seed = mParameters.getRandomSeed();
if (seed == 0) {
SecureRandom starter = new SecureRandom();
seed = starter.nextInt();
}
mLogger.logInfo("Picking random seed " + seed, true);
mRandom = new Random(seed);
if (mParameters.getTestCENC()) buildCENCTests();
if (mParameters.getTestCENS()) buildCENSTests();
if (mParameters.getTestCBC1()) buildCBC1Tests();
if (mParameters.getTestCBCS()) buildCBCSTests();
}
// The maximum number of subsamples required for the given resource rating tier.
int maxSubsamples() {
switch (mRRTier) {
default:
case 1:
return 10;
case 2:
return 16;
case 3:
return 32;
}
}
// The maximum sample size required for the given resource rating tier.
int maxSampleSize() {
int MB = 1024 * 1024;
switch (mRRTier) {
default:
case 1:
return 1 * MB;
case 2:
return 2 * MB;
case 3:
return 4 * MB;
}
}
// The maximum subsample size required for the given resource rating tier.
int maxSubsampleSize() {
int KB = 1024;
int MB = 1024 * 1024;
switch (mRRTier) {
default:
case 1:
return 100 * KB;
case 2:
return 500 * KB;
case 3:
return 1 * MB;
}
}
// Round up the given number to a multiple of pad.
final int roundUp(int originalSize, int pad) {
if (0 == pad) {
pad = AES_BLOCK_SIZE;
}
if ((originalSize % pad) > 0) {
return (originalSize + (pad - (originalSize % pad)));
} else {
return originalSize;
}
}
// Generate a random integer between 0 and max.
final int randomInt(int max) {
int x = mRandom.nextInt();
if (x < 0) x = -x;
return x % max;
}
// Decide if a pattern and mode combination should be tested or not and return true if the test
// should be skipped.
boolean forbidsPartialBlocks(MediaCodec.CryptoInfo.Pattern pattern, int mode) {
// By the CENC spec ISO/IEC 23001-7:2016, in sections 9.5.2.3 and 9.5.2.5, some modes and
// patterns require subsamples to end on an AES block boundary.
// (Thanks, Juce for reading the spec and figuring out which tests we can dump!)
// For "cens", which is CTR with a pattern, subsamples should end on a block boundary,
// so we throw out those tests. It is not worth testing the other case. The CENC spec
// says applications may prohibit these.
if (mode == MediaCodec.CRYPTO_MODE_AES_CTR && pattern.getEncryptBlocks() > 0) {
return true;
}
// Similarly, in "cbc1", subsamples SHALL end on a block boundary, so we throw out
// all those tests.
if (mode == MediaCodec.CRYPTO_MODE_AES_CBC && pattern.getEncryptBlocks() == 0) {
return true;
}
return false;
}
private void buildCENCTests() {
String description = "cenc";
MediaCodec.CryptoInfo.Pattern pattern = new MediaCodec.CryptoInfo.Pattern(0, 0);
int mode = MediaCodec.CRYPTO_MODE_AES_CTR;
buildTestsForMode(description, pattern, mode);
if (mParameters.getTestWrap()) {
buildWrappingIVTest(description, pattern, mode);
}
}
private void buildCENSTests() {
String description = "cens";
MediaCodec.CryptoInfo.Pattern pattern = new MediaCodec.CryptoInfo.Pattern(1, 9);
int mode = MediaCodec.CRYPTO_MODE_AES_CTR;
buildEncTests(description, pattern, mode);
buildTestsForModeRandomSizes(description, pattern, mode);
if (mParameters.getTestWrap()) {
buildWrappingIVTest(description, pattern, mode);
}
}
private void buildCBC1Tests() {
String description = "cbc1";
MediaCodec.CryptoInfo.Pattern pattern = new MediaCodec.CryptoInfo.Pattern(0, 0);
int mode = MediaCodec.CRYPTO_MODE_AES_CBC;
buildEncTests(description, pattern, mode);
buildTestsForModeRandomSizes(description, pattern, mode);
}
private void buildCBCSTests() {
String description = "cbcs";
MediaCodec.CryptoInfo.Pattern pattern = new MediaCodec.CryptoInfo.Pattern(1, 9);
int mode = MediaCodec.CRYPTO_MODE_AES_CBC;
buildTestsForMode(description, pattern, mode);
}
// Build tests for the given mode and skip pattern by generating a variety of sizes.
private void buildTestsForMode(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
buildTestsForModeSpecialSizes(description, pattern, mode);
buildTestsForModeRandomSizes(description, pattern, mode);
}
// Build some tests that have edge cases as the subsample sizes. These tests are
// also in the OEMCrypto unit tests.
private void buildTestsForModeSpecialSizes(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
// Note: The first test case has to have some encryption so that
// the CDM layer calls SelectKey before we try to compute a hash.
// That is because we don't necessarily compute a hash for CopyBuffer.
buildEncTests(description, pattern, mode);
buildClearTests(description, pattern, mode);
int[] pads = {0, 10, 25, 32};
for (int k = 0; k < pads.length; k++) {
buildNoOffsetTests(description, pattern, mode, pads[k]);
}
for (int k = 0; k < pads.length; k++) {
buildEvenOffsetTests(description, pattern, mode, pads[k]);
}
for (int k = 0; k < pads.length; k++) {
buildOddOffsetTests(description, pattern, mode, pads[k]);
}
buildPartialBlockTests(description, pattern, mode);
buildSmallSubsampleTests(description, pattern, mode);
if (mParameters.getTestMax()) {
buildMaxSampleTests(description, pattern, mode);
buildMaxSubSampleTests(description, pattern, mode);
}
}
// This tests the ability to decrypt multiple subsamples with no offset.
// There is no offset within the block, used by CTR mode. However, there might
// be an offset in the encrypt/skip pattern.
private void buildNoOffsetTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode, int pad) {
int[] clearSizes = {pad, pad, pad};
int[] encryptedSizes = {48, 64, roundUp(mMinSize, 16)};
buildTestsForModeAndSizes(
description + ", no offset", pattern, mode, clearSizes, encryptedSizes);
}
// This tests an offset into the block for the second encrypted subsample.
// This should only work for CTR mode, for CBC mode an error is expected in
// the decrypt step.
// If this test fails for CTR mode, then it is probably handling the
// block_offset incorrectly.
private void buildEvenOffsetTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode, int pad) {
int[] clearSizes = {pad, pad, pad};
int[] encryptedSizes = {16 + 8, 32, roundUp(mMinSize, 16)};
buildTestsForModeAndSizes(
description + ", even offset", pattern, mode, clearSizes, encryptedSizes);
}
// If the EvenOffset test passes, but this one doesn't, then DecryptCTR might
// be using the wrong definition of block offset. Adding the block offset to
// the block boundary should give you the beginning of the encrypted data.
// This should only work for CTR mode, for CBC mode, the block offset must be
// 0, so an error is expected in the decrypt step.
// Another way to view the block offset is with the formula:
// block_boundary + block_offset = beginning of subsample.
private void buildOddOffsetTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode, int pad) {
int[] clearSizes = {pad, pad, pad};
int[] encryptedSizes = {50, 75, roundUp(mMinSize, 16)};
buildTestsForModeAndSizes(
description + ", odd offset", pattern, mode, clearSizes, encryptedSizes);
}
// This tests the case where an encrypted sample is not an even number of
// blocks. For CTR mode, the partial block is encrypted. For CBC mode the
// partial block should be a copy of the clear data.
private void buildPartialBlockTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
int[] clearSizes = {mMinSize};
int[] encryptedSizes = {50};
buildTestsForModeAndSizes(
description + ", partial block", pattern, mode, clearSizes, encryptedSizes);
}
// A small subsample.
private void buildSmallSubsampleTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
int[] clearSizes = {5, mMinSize};
int[] encryptedSizes = {5, 0};
buildTestsForModeAndSizes(
description + ", small subsample", pattern, mode, clearSizes, encryptedSizes);
}
// The whole sample is clear.
private void buildClearTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
int[] clearSizes = {mMinSize};
int[] encryptedSizes = {0};
buildTestsForModeAndSizes(
description + ", all clear", pattern, mode, clearSizes, encryptedSizes);
}
// The whole sample is encrypted.
private void buildEncTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
int[] clearSizes = {0};
int[] encryptedSizes = {roundUp(mMinSize, 16)};
buildTestsForModeAndSizes(
description + ", all encrypted", pattern, mode, clearSizes, encryptedSizes);
}
// A test with maximum sample size.
private void buildMaxSampleTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
int subsize = maxSampleSize() / maxSubsamples();
int[] clearSizes = new int[maxSubsamples()];
int[] encryptedSizes = new int[maxSubsamples()];
for (int i = 0; i < maxSubsamples(); i++) {
clearSizes[i] = 0;
encryptedSizes[i] = subsize;
}
buildTestsForModeAndSizes(
description + ", max sample", pattern, mode, clearSizes, encryptedSizes);
}
// A test with maximum subsample size.
private void buildMaxSubSampleTests(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
int count = maxSampleSize() / maxSubsampleSize();
int[] clearSizes = new int[count];
int[] encryptedSizes = new int[count];
for (int i = 0; i < count; i++) {
clearSizes[i] = 0;
encryptedSizes[i] = maxSubsampleSize();
}
buildTestsForModeAndSizes(
description + ", max subsample", pattern, mode, clearSizes, encryptedSizes);
}
// Build a bunch of randomly sized buffers for testing different sizes. The
// user can select how many tests to run in the settings screen.
private void buildTestsForModeRandomSizes(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
int count = mParameters.getRandomCount();
boolean doRounding = forbidsPartialBlocks(pattern, mode);
for (int t = 0; t < count; t++) {
int numSubsamples = 1 + randomInt(maxSubsamples());
if (doRounding && !mParameters.getTestMultiSubsample()) {
// For the modes that require subsamples end on a block boundary,
// multisubsamples is rarely used. We don't run those tests
// unless explicitly requested.
numSubsamples = 1;
}
// Split the smallest into even pieces, and round up.
int base_size = mMinSize / numSubsamples + 1;
int[] clearSizes = new int[numSubsamples];
int[] encryptedSizes = new int[numSubsamples];
for (int i = 0; i < numSubsamples; i++) {
clearSizes[i] = randomInt(100);
encryptedSizes[i] = base_size + randomInt(600);
if (doRounding) {
encryptedSizes[i] = roundUp(encryptedSizes[i], AES_BLOCK_SIZE);
}
}
buildTestsForModeAndSizes(description, pattern, mode, clearSizes, encryptedSizes);
}
}
private boolean buildTestsForModeAndSizes(
String description,
MediaCodec.CryptoInfo.Pattern pattern,
int mode,
int[] clearSizes,
int[] encryptedSizes) {
// Throw out tests that are forbidden.
if (forbidsPartialBlocks(pattern, mode)) {
for (int i = 0; i < encryptedSizes.length; i++) {
if (encryptedSizes[i] % AES_BLOCK_SIZE != 0) return false;
}
}
// Test with a random iv.
byte[] iv = new byte[16];
mRandom.nextBytes(iv);
addTest(description, pattern, mode, clearSizes, encryptedSizes, iv);
return true;
}
// This is used to test that the IV wraps correctly in CTR mode.
private void buildWrappingIVTest(
String description, MediaCodec.CryptoInfo.Pattern pattern, int mode) {
int[] clearSizes = {0};
int[] encryptedSizes = {roundUp(mMinSize, AES_BLOCK_SIZE)};
byte[] iv = new byte[16];
mRandom.nextBytes(iv);
// Set the last 8 bytes to be near the wrap value:
for (int i = 8; i < 16; i++) iv[i] = (byte) 0xFF;
iv[15] = (byte) 0xFE;
addTest(description + ", wrapping iv", pattern, mode, clearSizes, encryptedSizes, iv);
}
private void addTest(
String description,
MediaCodec.CryptoInfo.Pattern pattern,
int mode,
int[] clearSizes,
int[] encryptedSizes,
byte[] iv) {
// Start counting frame number at 1, for human readability.
int frameNumber = mTests.size() + 1;
if (clearSizes.length != encryptedSizes.length) {
mLogger.setError("Malformed Test " + frameNumber);
return;
}
int testFrameSize = 0;
for (int i = 0; i < clearSizes.length; i++) {
testFrameSize += clearSizes[i];
testFrameSize += encryptedSizes[i];
}
MediaCodec.CryptoInfo info = new MediaCodec.CryptoInfo();
info.set(
clearSizes.length, clearSizes, encryptedSizes, LicenseHolder.TEST_KEY_ID, iv, mode);
info.setPattern(pattern);
mTests.add(new TestCase(frameNumber, description, testFrameSize, info, pattern));
}
final int getTestCount() {
return mTests.size();
}
// Get the test case for the specified index. This can be called
// multiple times for each frame.
final TestCase getTest(int index) {
return mTests.get(index);
}
}

View File

@@ -0,0 +1,191 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
class TestParameters {
private String mCodecName;
private boolean mUseLevel1 = false;
private boolean mTestCENC = true;
private boolean mTestCENS = false;
private boolean mTestCBCS = false;
private boolean mTestCBC1 = false;
private boolean mTestMax = false;
private boolean mTestWrap = false;
// TODO(b/139257871): This defaults to false until iv incrementing is corrected for cbc1.
private boolean mTestMultiSubsample = false;
private boolean mLogEachFrame = false;
private int mRandomCount = 5;
private int mRandomSeed = 0;
private int mTestFrame = -1;
// Create a minimal set of tests.
TestParameters() {
mCodecName = "default";
}
TestParameters(Context context, Logger logger) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
mCodecName = getCodecName(context, preferences);
mUseLevel1 = preferences.getBoolean(context.getString(R.string.key_use_level_1), true);
mTestCENC = preferences.getBoolean(context.getString(R.string.key_test_cenc), true);
mTestCENS = preferences.getBoolean(context.getString(R.string.key_test_cens), true);
mTestCBC1 = preferences.getBoolean(context.getString(R.string.key_test_cbc1), true);
mTestCBCS = preferences.getBoolean(context.getString(R.string.key_test_cbcs), true);
mTestMax = preferences.getBoolean(context.getString(R.string.key_test_max), false);
mTestWrap = preferences.getBoolean(context.getString(R.string.key_test_wrap), true);
mTestMultiSubsample =
preferences.getBoolean(context.getString(R.string.key_test_multi_subsample), false);
mLogEachFrame =
preferences.getBoolean(context.getString(R.string.key_log_each_frame), false);
String value =
preferences.getString(context.getString(R.string.key_random_test_count), "5000");
try {
mRandomCount = Integer.parseInt(value);
} catch (NumberFormatException e) {
logger.logException("Number format for preference " + value, e);
mRandomCount = 1000;
}
value = preferences.getString(context.getString(R.string.key_random_seed), "0");
try {
mRandomSeed = Integer.parseInt(value);
} catch (NumberFormatException e) {
mRandomSeed = 0; // Use 0 if string is empty or malformed.
}
value = preferences.getString(context.getString(R.string.key_test_frame), "0");
try {
mTestFrame = Integer.parseInt(value);
} catch (NumberFormatException e) {
mTestFrame = 0; // Use 0 if string is empty or malformed.
}
}
private String getCodecName(Context context, SharedPreferences preferences) {
// The user can specify a particular codec on the settings screen. If
// they leave it as default. Just to be clear, the default value is
// "default" and means we should get the codec by type instead of by
// name.
String codecName =
preferences.getString(
context.getString(R.string.key_codec),
context.getString(R.string.default_codec));
if (codecName == null
|| codecName.equals(context.getString(R.string.default_codec))
|| "".equals(codecName)) {
boolean secureCodec =
preferences.getBoolean(context.getString(R.string.key_use_secure_buffer), true);
codecName = MediaUtil.getCodecNameForMime(CodecHandler.MIME_TYPE, secureCodec);
}
return codecName;
}
public String getCodecName() {
return mCodecName;
}
public void setCodecName(String codecName) {
this.mCodecName = mCodecName;
}
public boolean getUseLevel1() {
return mUseLevel1;
}
public void setUseLevel1(boolean useLevel1) {
this.mUseLevel1 = useLevel1;
}
public boolean getTestCENC() {
return mTestCENC;
}
public void setTestCENC(boolean testCENC) {
this.mTestCENC = testCENC;
}
public boolean getTestCENS() {
return mTestCENS;
}
public void setTestCENS(boolean testCENS) {
this.mTestCENS = testCENS;
}
public boolean getTestCBCS() {
return mTestCBCS;
}
public void setTestCBCS(boolean testCBCS) {
this.mTestCBCS = testCBCS;
}
public boolean getTestCBC1() {
return mTestCBC1;
}
public void setTestCBC1(boolean testCBC1) {
this.mTestCBC1 = testCBC1;
}
public boolean getTestMax() {
return mTestMax;
}
public void setTestMax(boolean testMax) {
this.mTestMax = testMax;
}
public boolean getTestWrap() {
return mTestWrap;
}
public void setTestWrap(boolean testWrap) {
this.mTestWrap = testWrap;
}
public boolean getTestMultiSubsample() {
return mTestMultiSubsample;
}
public void setTestMultiSubsample(boolean testMultiSubsample) {
this.mTestMultiSubsample = testMultiSubsample;
}
public boolean getLogEachFrame() {
return mLogEachFrame;
}
public void setLogEachFrame(boolean logEachFrame) {
this.mLogEachFrame = logEachFrame;
}
public int getRandomCount() {
return mRandomCount;
}
public void setRandomCount(int randomCount) {
this.mRandomCount = mRandomCount;
}
public int getRandomSeed() {
return mRandomSeed;
}
public void setRandomSeed(int randomCount) {
this.mRandomSeed = mRandomSeed;
}
public int getTestFrame() {
return mTestFrame;
}
public void setTestFrame(int randomCount) {
this.mTestFrame = mTestFrame;
}
}

View File

@@ -0,0 +1,158 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import android.content.Context;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.view.SurfaceView;
// Helps initialize the other objects used in running the tests.
public class TestRunner {
private static final String TAG = "FDPT_TestRunner";
private Logger mLogger;
private Context mContext;
private SurfaceView mView;
private LicenseHolder mLicenseHolder;
private CodecHandler mCodecHandler;
private TestParameters mParameters;
TestRunner(Logger logger, Context context, SurfaceView view, TestParameters parameters) {
this.mLogger = logger;
this.mContext = context;
this.mView = view;
this.mParameters = parameters;
}
void cleanUp() {
if (mCodecHandler != null) {
mCodecHandler.cleanUp();
}
if (mLicenseHolder != null) {
mLicenseHolder.cleanUp();
}
}
boolean initLicense() {
mLicenseHolder = new LicenseHolder(mLogger, mParameters.getUseLevel1(), mContext);
if (mLicenseHolder.shouldSkipTest()) return false;
mLicenseHolder.getLicense();
if (!mLicenseHolder.hasLicense()) return false;
return true;
}
boolean initCodecHandler(MediaFormat format, IWorker worker) {
MediaCrypto crypto = mLicenseHolder.getMediaCrypto();
if (crypto == null) return false;
mCodecHandler = new CodecHandler(mLogger, crypto, mView, format, worker);
mCodecHandler.createCodecByName(mParameters.getCodecName());
if (!mCodecHandler.start()) return false;
return true;
}
// Run by the worker thread to do one test.
void doTest(IWorker worker) throws InterruptedException {
if (worker.finishEarly()) return;
mLogger.clearStatus();
mLogger.setStatus("Start test.");
if (!initLicense()) return;
FrameGenerator frameGenerator = new FrameGenerator(mContext, mLogger);
if (frameGenerator == null) return;
frameGenerator.prepareKeyFrame();
HashGenerator hashGenerator = new HashGenerator();
Encryptor encryptor = new Encryptor(mLicenseHolder, mLogger);
if (!initCodecHandler(frameGenerator.getMediaFormat(), worker)) return;
mLogger.setStatus("Codec ready.");
int minSize = frameGenerator.getMinimumFrameSize();
if (minSize <= 0) return;
int tier = mLicenseHolder.getResourceRatingTier();
TestCaseFactory mTestCaseFactory =
new TestCaseFactory(
mLogger, minSize,
tier, mParameters);
int testCount = mTestCaseFactory.getTestCount();
mLogger.setStatus("Running");
String currentDescription = "init";
mLogger.setStatus(-1, testCount); // Indicate no tests have passed.
if (testCount < 1) {
mLogger.setStatus("No test cases selected.");
mLogger.logInfo("Go to Setup page and select tests before clicking 'Start Test'", true);
return;
}
for (int i = 0; i < testCount; i++) {
worker.maybePause();
if (worker.finishEarly()) return;
TestCase test = mTestCaseFactory.getTest(i);
if (!currentDescription.equals(test.getDescription())) {
currentDescription = test.getDescription();
mLogger.setStatus("Testing " + currentDescription);
}
if (mParameters.getLogEachFrame()) {
test.logInfo(mLogger);
}
ByteArrayFrameBuilder fb = new ByteArrayFrameBuilder(test.getBufferSize());
byte[] clearFrame = frameGenerator.buildtestFrame(fb);
if (clearFrame == null) {
mLogger.setError("Frame generation failed.");
test.logInfo(mLogger);
return;
}
if (clearFrame.length != test.getBufferSize()) {
mLogger.setError(
"Frame generation size = "
+ clearFrame.length
+ ", expected "
+ test.getBufferSize());
test.logInfo(mLogger);
return;
}
byte[] hash = hashGenerator.getHash(clearFrame, test);
if (hash == null) {
mLogger.setError("Hash generation failed.");
test.logInfo(mLogger);
return;
}
if (!mLicenseHolder.sendHash(hash, test, worker)) return;
byte[] encryptedFrame = encryptor.encryptFrame(clearFrame, test);
if (encryptedFrame == null) {
mLogger.setError("Encryption failed.");
test.logInfo(mLogger);
return;
}
boolean lastFrame = (i + 1 == testCount);
if (!mCodecHandler.sendData(test, clearFrame, encryptedFrame, lastFrame)) {
test.logInfo(mLogger);
return;
}
mLogger.setStatus(i, testCount);
if (mLicenseHolder.foundError(mTestCaseFactory, worker)) {
worker.handleError(mLogger.getErrorString());
mLogger.logInfo("got hash error, frame " + test.getFrameNumber(), true);
return;
}
}
mLogger.setStatus("Finishing test.");
mCodecHandler.finishFrames();
if (mLicenseHolder.foundError(mTestCaseFactory, worker)) {
worker.handleError(mLogger.getErrorString());
return;
}
mLogger.setStatus("Test finished.");
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
// This is the main worker thread that runs each test case.
class Worker extends Thread implements IWorker {
private static final String TAG = "FDPT_Worker";
private boolean mFinished = false; // True when the main application is done.
private boolean mPaused = true; // True if the main activity is not visible.
private boolean mRunTest = false; // True if a test is running or should start.
private TestRunner mTestRunner;
private Logger mLogger;
Worker(Logger logger) {
super("Worker Thread");
this.mLogger = logger;
}
@Override
public void run() {
try {
while (!mFinished) {
if (shouldStartTest()) {
mTestRunner.doTest(this);
finishTest();
}
}
} catch (InterruptedException ex) {
mLogger.logInfo("worker interrupted: " + ex.getMessage(), true);
}
}
// This is called by the main activity when the application is not visible.
synchronized void onPause() {
mPaused = true;
notifyAll();
}
// This is called by the main activity when the application becomes visible.
synchronized void onResume() {
mPaused = false;
notifyAll();
}
// This is called by the main activity when the application closes.
synchronized void onStop() {
mFinished = true;
notifyAll();
}
// This is called by the UI thread to start running a test.
synchronized void startTest(TestRunner testRunner) {
if (mRunTest) {
mLogger.logInfo("Test already started.", true);
} else {
mRunTest = true;
mTestRunner = testRunner;
}
notifyAll();
}
// This is called by the UI thread to cancel a running test.
// Run by worker thread.
synchronized void cancelTest() {
if (!mRunTest) {
mLogger.logInfo("Test not running.", true);
} else {
mRunTest = false;
}
notifyAll();
}
// This is called by the worker to see if a test should be started.
// It blocks until a test is started or the application ends.
// Run by worker thread.
private synchronized boolean shouldStartTest() {
if (!mFinished && !mRunTest) {
try {
wait();
} catch (InterruptedException e) {
mLogger.logInfo("Worker Thread interrupted.", true);
}
}
return !mFinished && mRunTest;
}
// Check to see if the app is finished or the test has been stopped.
// Run by worker thread.
@Override
public synchronized boolean finishEarly() {
if (mFinished || !mRunTest) {
mLogger.setStatus("Test aborted.");
return true;
}
return false;
}
@Override
public void handleException(String message, Exception e) {
mLogger.logException(message, e);
}
@Override
public void handleError(String message) {}
// This blocks until mPaused is false, or the test is stopped.
// Run by worker thread.
@Override
public synchronized void maybePause() {
// Also finish pausing when the app is done or the test has been stopped.
while (mPaused && !mFinished && mRunTest) {
try {
wait();
} catch (InterruptedException e) {
mLogger.logInfo("Worker Thread interrupted.", true);
}
}
}
// Finish the test and clean up.
synchronized void finishTest() {
if (mTestRunner != null) {
mTestRunner.cleanUp();
}
mRunTest = false;
}
}

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

View File

@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.google.widevine.fulldecryptpathtesting.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:visibility="visible">
<Button
android:id="@+id/button_setup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="configureTest"
android:text="@string/setup"
android:minWidth="60dp"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
android:layout_marginEnd="0dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:paddingBottom="0dp"
android:paddingEnd="0dp"
android:paddingLeft="0dp"
android:paddingRight="0dp"
android:paddingStart="0dp"
android:paddingTop="0dp"
android:textAppearance="?android:attr/textAppearanceSmall"
/>
<Button
android:id="@+id/button_start"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:layout_marginEnd="0dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:layout_marginTop="0dp"
android:minWidth="60dp"
android:onClick="startTest"
android:paddingBottom="0dp"
android:paddingEnd="0dp"
android:paddingLeft="0dp"
android:paddingRight="0dp"
android:paddingStart="0dp"
android:paddingTop="0dp"
android:text="@string/play"
android:textAppearance="?android:attr/textAppearanceSmall"
/>
<Button
android:id="@+id/button_stop"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:layout_marginEnd="0dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:layout_marginTop="0dp"
android:minWidth="60dp"
android:onClick="stopTest"
android:paddingBottom="0dp"
android:paddingEnd="0dp"
android:paddingLeft="0dp"
android:paddingRight="0dp"
android:paddingStart="0dp"
android:paddingTop="0dp"
android:text="@string/stop"
android:textAppearance="?android:attr/textAppearanceSmall"
/>
<Button
android:id="@+id/button_clear"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginBottom="0dp"
android:layout_marginEnd="0dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:layout_marginTop="0dp"
android:minWidth="60dp"
android:onClick="clearLog"
android:paddingBottom="0dp"
android:paddingEnd="0dp"
android:paddingLeft="0dp"
android:paddingRight="0dp"
android:paddingStart="0dp"
android:paddingTop="0dp"
android:text="@string/clear"
android:textAppearance="?android:attr/textAppearanceSmall"
/>
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_weight="1"
android:orientation="horizontal"
android:visibility="visible">
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="status" />
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2"
android:orientation="vertical"
android:visibility="visible">
<SurfaceView
android:id="@+id/playback_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1" />
<ScrollView
android:id="@+id/log_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:fillViewport="true">
<TextView
android:id="@+id/log_output"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="No output yet." />
</ScrollView>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<item android:id="@+id/menu_setup"
android:icon="@android:drawable/ic_menu_preferences"
android:title="@string/setup"
android:showAsAction="ifRoom|withText"
tools:ignore="AppCompatResource"/>
<item android:id="@+id/menu_play"
android:icon="@android:drawable/ic_media_play"
android:title="@string/play"
android:showAsAction="ifRoom|withText"
tools:ignore="AppCompatResource"/>
</menu>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#402174</color>
</resources>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MissingTranslation">
<!-- Keys used for preferences. -->
<string name="key_test_cenc">test_cenc</string>
<string name="key_test_cens">test_cens</string>
<string name="key_test_cbc1">test_cbc1</string>
<string name="key_test_cbcs">test_cbcs</string>
<string name="key_test_max">test_max</string>
<string name="key_test_wrap">test_wrap</string>
<string name="key_test_multi_subsample">test_multi_subsample</string>
<string name="key_log_each_frame">test_log_each_frame</string>
<string name="key_random_test_count">random_test_count</string>
<string name="key_random_seed">random_seed</string>
<string name="key_test_frame">random_test_count</string>
<string name="key_use_level_1">use_level_1</string>
<string name="key_use_secure_buffer">use_secure_buffer</string>
<string name="key_codec">codec</string>
<string name="default_codec">default</string>
<!-- Keys used for MediaDrm.getPropertyString. -->
<string name="key_security_level">securityLevel</string>
<string name="key_system_id">systemId</string>
<string name="key_cdm_version">version</string>
<string name="key_oemcrypto_api">oemCryptoApiVersion</string>
<string name="key_oemcrypto_build_info">oemCryptoBuildInformation</string>
<string name="key_hash_support">decryptHashSupport</string>
<string name="key_resource_rating">resourceRatingTier</string>
</resources>

View File

@@ -0,0 +1,89 @@
<resources>
<string name="app_name">Full Decrypt Path Testing</string>
<string name="short_app_name">FDPT</string>
<string name="setup">Setup</string>
<string name="play">Start Test</string>
<string name="stop">Stop Test</string>
<string name="clear">Clear</string>
<string name="title_activity_settings">Settings</string>
<!-- Strings related to properties that are displayed -->
<string name="security_level">Security Level</string>
<string name="system_id">System ID</string>
<string name="cdm_version">Widevine Plugin Version</string>
<string name="oemcrypto_api">OEMCrypto API version</string>
<string name="oemcrypto_build_info">OEMCrypto Build Info</string>
<string name="hash_support">Decrypt Hash Supported</string>
<string name="resource_rating">Resource Rating Tier</string>
<!-- Strings related to Settings -->
<string name="pref_main_category">Full Decrypt Path Testing Setup</string>
<string name="pref_test_cenc">Test \"cenc\"</string>
<string name="pref_test_cenc_summary">Run tests with CTR mode and no pattern</string>
<string name="pref_test_cens">Test \"cens\"</string>
<string name="pref_test_cens_summary">Run tests with CTR mode with pattern (rare)</string>
<string name="pref_test_cbc1">Test \"cbc1\"</string>
<string name="pref_test_cbc1_summary">Run tests with CBC mode and no pattern (rare)</string>
<string name="pref_test_cbcs">Test \"cbcs\"</string>
<string name="pref_test_cbcs_summary">Run tests with CBC mode with pattern</string>
<string name="pref_test_max">Test Maximum Buffer Sizes</string>
<string name="pref_test_max_summary">Run tests with maximum sample
and subsample sizes for OEMCrypto. This may cause problems with the codec if it cannot
handle OEMCrypto\'s maximum buffer size.</string>
<string name="pref_test_wrap">Test IV overflow</string>
<string name="pref_test_wrap_summary">Run tests with an IV in counter mode that
overflows when incremented.</string>
<string name="pref_test_multi_subsample">Test Multisubsample for cens and cbc1</string>
<string name="pref_test_multi_subsample_summary">If true, tests are run which include
more than one subsample for \"cens\" and \"cbc1\". A bug in the CDM layer may
prevent \"cbc1\" test from passing.</string>
<string name="pref_log_each_frame">Log Each Frame</string>
<string name="pref_log_each_frame_summary">If true, then a description of each
frame is logged to the output window. This is very verbose, and will slow
the tests down significantly. However, when debugging, it might help to see
descriptions of tests that pass.</string>
<string name="pref_random_test_count">Random Test Count</string>
<string name="pref_random_test_count_summary">Click to set how many random tests
to run for each encryption scheme.</string>
<string name="pref_random_seed">Random Seed</string>
<string name="pref_random_seed_summary">If set, this is used to seed the test
factory\'s random number generator. The seed may be found in the logcat. This
allows a user to repeate a previous test.</string>
<string name="pref_test_frame">Test Frame</string>
<string name="pref_test_frame_summary">if set, only thiis frame will be tested.
This allows a user to isolate and debug a single failing test case.</string>
<string name="pref_use_level_1">Use Level 1</string>
<string name="pref_use_level_1_summary">If this is set, the L1 OEMCrypto is used</string>
<string name="pref_use_secure_buffer">Use Secure Buffer</string>
<string name="pref_use_secure_buffer_summary">If the default codec is used,
then this determines if it is secure or not.</string>
<string name="pref_codec">Codec</string>
<string name="pref_codec_summary">The name of the Codec to use.
If this is not set, then a default codec is chosen for mime type video/avc.
The user must make sure it is for video and uses a secure buffer
if required.\n
current = %1$s</string>
<string name="default_codec_description">Use default codec for video mime type.</string>
</resources>
<!-- Note to translators:
The following abbreviations are only used by Widevine. They probably should NOT be translated.
FDPT = an abbreviation of "Full Decrypt Path Testing", which is this application.
L1 = Level 1.
The following abbreviations are documented here: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
They are familiar to most engineers who would use this application.
CTR = Counter
CBC = Cipher Block Chaining.
IV = initialization vector.
"overflow" refers to integer overflow, as documented here:
https://en.wikipedia.org/wiki/Integer_overflow
"hash" refers to https://en.wikipedia.org/wiki/Hash_function
"secure buffer" refers to the type of buffer used in Android, as documented in the MediaCodec:
https://developer.android.com/reference/android/media/MediaCodec#creating-secure-decoders
-->

View File

@@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View File

@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_key_main_category"
android:title="@string/pref_main_category">
<SwitchPreference
android:key="@string/key_use_level_1"
android:title="@string/pref_use_level_1"
android:summary="@string/pref_use_level_1_summary"
android:defaultValue="true"
/>
<SwitchPreference
android:key="@string/key_use_secure_buffer"
android:title="@string/pref_use_secure_buffer"
android:summary="@string/pref_use_secure_buffer_summary"
android:defaultValue="true"
/>
<com.google.widevine.fulldecryptpathtesting.CodecPreference
android:key="@string/key_codec"
android:title="@string/pref_codec"
android:summary="@string/pref_codec_summary"
android:defaultValue="@string/default_codec"
/>
<EditTextPreference
android:key="@string/key_random_test_count"
android:title="@string/pref_random_test_count"
android:summary="@string/pref_random_test_count_summary"
android:inputType="number"
android:defaultValue="5000"
/>
<SwitchPreference
android:key="@string/key_test_cenc"
android:title="@string/pref_test_cenc"
android:summary="@string/pref_test_cenc_summary"
android:defaultValue="true"
/>
<SwitchPreference
android:key="@string/key_test_cens"
android:title="@string/pref_test_cens"
android:summary="@string/pref_test_cens_summary"
android:defaultValue="true"
/>
<SwitchPreference
android:key="@string/key_test_cbc1"
android:title="@string/pref_test_cbc1"
android:summary="@string/pref_test_cbc1_summary"
android:defaultValue="false"
/>
<SwitchPreference
android:key="@string/key_test_cbcs"
android:title="@string/pref_test_cbcs"
android:summary="@string/pref_test_cbcs_summary"
android:defaultValue="true"
/>
<SwitchPreference
android:key="@string/key_test_max"
android:title="@string/pref_test_max"
android:summary="@string/pref_test_max_summary"
android:defaultValue="false"
/>
<SwitchPreference
android:key="@string/key_test_wrap"
android:title="@string/pref_test_wrap"
android:summary="@string/pref_test_wrap_summary"
android:defaultValue="true"
/>
<!-- TODO(b/139257871) This defaults to false because cbc1 is not -->
<!-- handled correctly on most versions of Android at the CDM layer. -->
<SwitchPreference
android:key="@string/key_test_multi_subsample"
android:title="@string/pref_test_multi_subsample"
android:summary="@string/pref_test_multi_subsample_summary"
android:defaultValue="false"
/>
<SwitchPreference
android:key="@string/key_log_each_frame"
android:title="@string/pref_log_each_frame"
android:summary="@string/pref_log_each_frame_summary"
android:defaultValue="false"
/>
<EditTextPreference
android:key="@string/key_random_seed"
android:title="@string/pref_random_seed"
android:summary="@string/pref_random_seed_summary"
android:inputType="number"
android:defaultValue=""
/>
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,21 @@
// Copyright 2019 Google LLC. All Rights Reserved. This file and proprietary
// source code may only be used and distributed under the Widevine Master
// License Agreement.
package com.google.widevine.fulldecryptpathtesting;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@@ -0,0 +1 @@
include ':mobile'