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
95
libwvdrmengine/test/java/FullDecryptPathTesting/.gitignore
vendored
Normal 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
|
||||
9
libwvdrmengine/test/java/FullDecryptPathTesting/.idea/modules.xml
generated
Normal 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>
|
||||
12
libwvdrmengine/test/java/FullDecryptPathTesting/.idea/runConfigurations.xml
generated
Normal 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>
|
||||
90
libwvdrmengine/test/java/FullDecryptPathTesting/README.md
Normal 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.
|
||||
|
||||
27
libwvdrmengine/test/java/FullDecryptPathTesting/build.gradle
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
BIN
libwvdrmengine/test/java/FullDecryptPathTesting/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
libwvdrmengine/test/java/FullDecryptPathTesting/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||
160
libwvdrmengine/test/java/FullDecryptPathTesting/gradlew
vendored
Executable 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 "$@"
|
||||
90
libwvdrmengine/test/java/FullDecryptPathTesting/gradlew.bat
vendored
Normal 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
|
||||
1
libwvdrmengine/test/java/FullDecryptPathTesting/mobile/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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} )
|
||||
@@ -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'
|
||||
}
|
||||
21
libwvdrmengine/test/java/FullDecryptPathTesting/mobile/proguard-rules.pro
vendored
Normal 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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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_
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 755 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 934 B |
|
After Width: | Height: | Size: 601 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#402174</color>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
-->
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
include ':mobile'
|
||||