From 67bc9ba9336a3193defd2267026af1324773c8b7 Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 13:30:08 -0500 Subject: [PATCH 01/20] Add new .gitignore --- .gitignore | 81 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 91ed47b..3d80727 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,77 @@ -### Android ### -# Built application files -*.apk -*.ap_ +############################################################################# +# +# .gitignore declares what files should be ignored by git +# +# This particular file is a composite of github's Android and Eclipse +# files, along with some custom additions. +# +############################################################################# + +############################################################################# +# Eclipse related files +############################################################################# + +*.pydevproject +.metadata +.gradle +bin/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath -# Files for the Dalvik VM -*.dex +# sbteclipse plugin +.target -# Java class files -*.class +# TeXlipse plugin +.texlipse + +############################################################################# +# Android related files +############################################################################# + +# Built application files +*.apk +#*.ap_ # Generated files -bin/ gen/ # Gradle files -.gradle/ build/ -/*/build/ - -# Local configuration file (sdk path, etc) -local.properties # Proguard folder generated by Eclipse proguard/ -# Log Files +#Log Files *.log -### Android Patch ### -gen-external-apklibs +############################################################################# +# Other Misc. files +############################################################################# -## Directory-based project format: -.idea/ -*.db +# Temp files for KDE and other Editor's +*~ +############################################################################# +# Android Studio related files +############################################################################# + +.idea/ +#gradle.properties +*.iml \ No newline at end of file From 8c851c16140685891d23d8c4281258f2229a9ab2 Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 13:30:44 -0500 Subject: [PATCH 02/20] Remove unnecesary files. --- PiracyChecker.iml | 19 ----- app/app.iml | 134 --------------------------------- gradlew | 0 gradlew.bat | 180 ++++++++++++++++++++++---------------------- library/library.iml | 111 --------------------------- 5 files changed, 90 insertions(+), 354 deletions(-) delete mode 100644 PiracyChecker.iml delete mode 100644 app/app.iml mode change 100755 => 100644 gradlew delete mode 100644 library/library.iml diff --git a/PiracyChecker.iml b/PiracyChecker.iml deleted file mode 100644 index aec6437..0000000 --- a/PiracyChecker.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/app.iml b/app/app.iml deleted file mode 100644 index c9418c5..0000000 --- a/app/app.iml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/gradlew.bat b/gradlew.bat index aec9973..8a0b282 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +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 +@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 diff --git a/library/library.iml b/library/library.iml deleted file mode 100644 index 658d937..0000000 --- a/library/library.iml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 446c5f0aadee063aca0e9a7c865a869d44b28443 Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 13:59:59 -0500 Subject: [PATCH 03/20] Update libraries and code. --- app/build.gradle | 19 +- build.gradle | 2 +- library/build.gradle | 17 +- library/src/main/AndroidManifest.xml | 4 +- .../licensing/ILicenseResultListener.aidl | 21 + .../vending/licensing/ILicensingService.aidl | 23 + .../piracychecker/PiracyChecker.java | 72 +- .../piracychecker/UtilsLibrary.java | 33 +- .../enums/PiracyCheckerCallback.java | 11 +- .../enums/PiracyCheckerError.java | 2 +- .../vending/licensing/AESObfuscator.java | 28 +- .../vending/licensing/APKExpansionPolicy.java | 188 ++-- .../vending/licensing/DeviceLimiter.java | 28 +- .../licensing/ILicenseResultListener.java | 99 -- .../vending/licensing/ILicensingService.java | 99 -- .../vending/licensing/LicenseChecker.java | 254 ++--- .../licensing/LicenseCheckerCallback.java | 64 +- .../vending/licensing/LicenseValidator.java | 33 +- .../vending/licensing/NullDeviceLimiter.java | 10 +- .../android/vending/licensing/Obfuscator.java | 21 +- .../android/vending/licensing/Policy.java | 27 +- .../licensing/PreferenceObfuscator.java | 4 +- .../vending/licensing/ResponseData.java | 36 +- .../licensing/ServerManagedPolicy.java | 123 +-- .../vending/licensing/StrictPolicy.java | 28 +- .../licensing/ValidationException.java | 14 +- .../vending/licensing/util/Base64.java | 980 +++++++++--------- .../util/Base64DecoderException.java | 14 +- .../licensing/util/URIQueryDecoder.java | 60 ++ 29 files changed, 1086 insertions(+), 1228 deletions(-) create mode 100644 library/src/main/aidl/com/android/vending/licensing/ILicenseResultListener.aidl create mode 100644 library/src/main/aidl/com/android/vending/licensing/ILicensingService.aidl delete mode 100644 library/src/main/java/com/google/android/vending/licensing/ILicenseResultListener.java delete mode 100644 library/src/main/java/com/google/android/vending/licensing/ILicensingService.java create mode 100644 library/src/main/java/com/google/android/vending/licensing/util/URIQueryDecoder.java diff --git a/app/build.gradle b/app/build.gradle index 3086032..aeda5a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,13 +1,13 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 23 - buildToolsVersion "23.0.2" + compileSdkVersion 25 + buildToolsVersion "25.0.2" defaultConfig { applicationId "com.github.javiersantos.piracychecker.demo" - minSdkVersion 11 - targetSdkVersion 23 + minSdkVersion 14 + targetSdkVersion 25 versionCode 1 versionName "1.0" } @@ -21,10 +21,9 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.4.0' - compile 'com.android.support:design:23.4.0' - compile 'com.mikepenz:iconics-core:2.5.11@aar' - compile 'com.mikepenz:material-design-iconic-typeface:2.2.0.1@aar' - compile('com.mikepenz:aboutlibraries:5.6.6@aar') { transitive = true } + compile 'com.android.support:design:25.1.0' + compile 'com.mikepenz:iconics-core:2.8.2@aar' + compile 'com.mikepenz:material-design-iconic-typeface:2.2.0.2@aar' + compile('com.mikepenz:aboutlibraries:5.9.1@aar') { transitive = true } compile project(':library') -} +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 03bced9..f551c5f 100644 --- a/build.gradle +++ b/build.gradle @@ -20,4 +20,4 @@ allprojects { task clean(type: Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index c1df72d..1c68050 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,15 +1,14 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "23.0.2" - useLibrary 'org.apache.http.legacy' + compileSdkVersion 25 + buildToolsVersion "25.0.2" defaultConfig { - minSdkVersion 8 - targetSdkVersion 23 - versionCode 1 - versionName "0.0.2" + minSdkVersion 9 + targetSdkVersion 25 + versionCode 3 + versionName "0.0.3" } buildTypes { release { @@ -21,5 +20,5 @@ android { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:23.4.0' -} + compile 'com.android.support:appcompat-v7:25.1.0' +} \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 4ec9f51..c619089 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,2 +1,4 @@ + package="com.github.javiersantos.piracychecker" > + + \ No newline at end of file diff --git a/library/src/main/aidl/com/android/vending/licensing/ILicenseResultListener.aidl b/library/src/main/aidl/com/android/vending/licensing/ILicenseResultListener.aidl new file mode 100644 index 0000000..869cb16 --- /dev/null +++ b/library/src/main/aidl/com/android/vending/licensing/ILicenseResultListener.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.licensing; + +oneway interface ILicenseResultListener { + void verifyLicense(int responseCode, String signedData, String signature); +} diff --git a/library/src/main/aidl/com/android/vending/licensing/ILicensingService.aidl b/library/src/main/aidl/com/android/vending/licensing/ILicensingService.aidl new file mode 100644 index 0000000..9541a20 --- /dev/null +++ b/library/src/main/aidl/com/android/vending/licensing/ILicensingService.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.licensing; + +import com.android.vending.licensing.ILicenseResultListener; + +oneway interface ILicensingService { + void checkLicense(long nonce, String packageName, in ILicenseResultListener listener); +} diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/PiracyChecker.java b/library/src/main/java/com/github/javiersantos/piracychecker/PiracyChecker.java index 3191c6b..1c361c1 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/PiracyChecker.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/PiracyChecker.java @@ -1,5 +1,11 @@ package com.github.javiersantos.piracychecker; +import com.google.android.vending.licensing.AESObfuscator; +import com.google.android.vending.licensing.LicenseChecker; +import com.google.android.vending.licensing.LicenseCheckerCallback; +import com.google.android.vending.licensing.ServerManagedPolicy; + +import android.annotation.SuppressLint; import android.content.Context; import android.provider.Settings; import android.support.annotation.StringRes; @@ -7,22 +13,23 @@ import com.github.javiersantos.piracychecker.enums.InstallerID; import com.github.javiersantos.piracychecker.enums.PiracyCheckerCallback; import com.github.javiersantos.piracychecker.enums.PiracyCheckerError; -import com.google.android.vending.licensing.AESObfuscator; -import com.google.android.vending.licensing.LicenseChecker; -import com.google.android.vending.licensing.LicenseCheckerCallback; -import com.google.android.vending.licensing.ServerManagedPolicy; import java.util.ArrayList; import java.util.List; +@SuppressLint("HardwareIds") public class PiracyChecker { - private Context context; - private String unlicensedDialogTitle, unlicensedDialogDescription; - private boolean enableLVL, enableSigningCertificate, enableInstallerId; - private String licenseBase64; - private String signature; - private List installerIDs; - private PiracyCheckerCallback callback; + + protected Context context; + protected String unlicensedDialogTitle; + protected String unlicensedDialogDescription; + protected boolean enableLVL; + protected boolean enableSigningCertificate; + protected boolean enableInstallerId; + protected String licenseBase64; + protected String signature; + protected List installerIDs; + protected PiracyCheckerCallback callback; public PiracyChecker(Context context) { this.context = context; @@ -71,25 +78,31 @@ public void start() { else verify(new PiracyCheckerCallback() { @Override - public void allow() {} + public void allow() { + } @Override public void dontAllow(PiracyCheckerError error) { - UtilsLibrary.showUnlicensedDialog(context, unlicensedDialogTitle, unlicensedDialogDescription).show(); + UtilsLibrary.buildUnlicensedDialog(context, unlicensedDialogTitle, + unlicensedDialogDescription).show(); } }); } - private void verify(final PiracyCheckerCallback verifyCallback) { - // Library will verify first the non-LVL methods since LVL is asynchronous and could take some seconds to give a result + protected void verify(final PiracyCheckerCallback verifyCallback) { + // Library will verify first the non-LVL methods since LVL is asynchronous and could take + // some seconds to give a result if (!verifySigningCertificate()) { verifyCallback.dontAllow(PiracyCheckerError.SIGNATURE_NOT_VALID); } else if (!verifyInstallerId()) { verifyCallback.dontAllow(PiracyCheckerError.INVALID_INSTALLER_ID); } else { if (enableLVL) { - String deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); - LicenseChecker licenseChecker = new LicenseChecker(context, new ServerManagedPolicy(context, new AESObfuscator(UtilsLibrary.SALT, context.getPackageName(), deviceId)), licenseBase64); + String deviceId = Settings.Secure.getString(context.getContentResolver(), + Settings.Secure.ANDROID_ID); + LicenseChecker licenseChecker = new LicenseChecker(context, new + ServerManagedPolicy(context, new AESObfuscator(UtilsLibrary.SALT, context + .getPackageName(), deviceId)), licenseBase64); licenseChecker.checkAccess(new LicenseCheckerCallback() { @Override public void allow(int reason) { @@ -102,7 +115,8 @@ public void dontAllow(int reason) { } @Override - public void applicationError(int errorCode) {} + public void applicationError(int errorCode) { + } }); } else { verifyCallback.allow(); @@ -110,32 +124,26 @@ public void applicationError(int errorCode) {} } } - private boolean verifySigningCertificate() { - boolean signingVerifyValid = false; - + protected boolean verifySigningCertificate() { if (enableSigningCertificate) { if (UtilsLibrary.verifySigningCertificate(context, signature)) { - signingVerifyValid = true; + return true; } } else { - signingVerifyValid = true; + return true; } - - return signingVerifyValid; + return false; } - private boolean verifyInstallerId() { - boolean installerIdValid = false; - + protected boolean verifyInstallerId() { if (enableInstallerId) { if (UtilsLibrary.verifyInstallerId(context, installerIDs)) { - installerIdValid = true; + return true; } } else { - installerIdValid = true; + return true; } - - return installerIdValid; + return false; } } diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/UtilsLibrary.java b/library/src/main/java/com/github/javiersantos/piracychecker/UtilsLibrary.java index 3776460..fa9b7a5 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/UtilsLibrary.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/UtilsLibrary.java @@ -1,5 +1,6 @@ package com.github.javiersantos.piracychecker; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; @@ -16,40 +17,43 @@ import java.util.ArrayList; import java.util.List; +@SuppressLint("PackageManagerGetSignatures") class UtilsLibrary { - static final byte[] SALT = new byte[] { + static final byte[] SALT = new byte[]{ -46, 65, 30, -128, -103, -57, 74, -64, 51, 88, -95, -45, 77, -117, -36, -113, -11, 32, -64, 89 }; - static AlertDialog showUnlicensedDialog(Context context, String title, String content) { + static AlertDialog buildUnlicensedDialog(Context context, String title, String content) { final Activity activity = (Activity) context; - return new AlertDialog.Builder(context) .setCancelable(false) .setTitle(title) .setMessage(content) - .setPositiveButton(context.getString(R.string.app_unlicensed_close), new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialogInterface, int i) { - if (activity.isFinishing()) - return; - activity.finish(); - } - }).create(); + .setPositiveButton(context.getString(R.string.app_unlicensed_close), new + DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + if (activity.isFinishing()) + return; + activity.finish(); + } + }).create(); } static String getCurrentSignature(Context context) { String res = ""; try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context + .getPackageName(), PackageManager.GET_SIGNATURES); for (Signature signature : packageInfo.signatures) { MessageDigest messageDigest = MessageDigest.getInstance("SHA"); messageDigest.update(signature.toByteArray()); res = Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT); } - } catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException ignored) {} + } catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException ignored) { + } return res.trim(); } @@ -60,7 +64,8 @@ static boolean verifySigningCertificate(Context context, String appSignature) { static boolean verifyInstallerId(Context context, List installerID) { List validInstallers = new ArrayList<>(); - final String installer = context.getPackageManager().getInstallerPackageName(context.getPackageName()); + final String installer = context.getPackageManager().getInstallerPackageName(context + .getPackageName()); for (InstallerID id : installerID) { validInstallers.addAll(id.toIDs()); diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerCallback.java b/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerCallback.java index e7b58d6..00f1d6f 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerCallback.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerCallback.java @@ -1,15 +1,18 @@ package com.github.javiersantos.piracychecker.enums; -import com.github.javiersantos.piracychecker.enums.PiracyCheckerError; - public interface PiracyCheckerCallback { /** - * allow method called after the app is valid and licensed - * dontAllow method called if the app is not valid or the user is using an unlicensed version + * Called after the app checked as valid and licensed */ void allow(); + /** + * Called if the app is not valid or the user is using an unlicensed version + * + * @param error PiracyCheckerError.NOT_LICENSED, PiracyCheckerError.SIGNATURE_NOT_VALID or + * PiracyCheckerError.INVALID_INSTALLER_ID + */ void dontAllow(PiracyCheckerError error); } diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerError.java b/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerError.java index 9edf7aa..ab15478 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerError.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerError.java @@ -19,4 +19,4 @@ public String toString() { return text; } -} +} \ No newline at end of file diff --git a/library/src/main/java/com/google/android/vending/licensing/AESObfuscator.java b/library/src/main/java/com/google/android/vending/licensing/AESObfuscator.java index ee12c68..21f9f94 100644 --- a/library/src/main/java/com/google/android/vending/licensing/AESObfuscator.java +++ b/library/src/main/java/com/google/android/vending/licensing/AESObfuscator.java @@ -40,23 +40,23 @@ public class AESObfuscator implements Obfuscator { private static final String KEYGEN_ALGORITHM = "PBEWITHSHAAND256BITAES-CBC-BC"; private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; private static final byte[] IV = - { 16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74 }; - private static final String header = "com.android.vending.licensing.AESObfuscator-1|"; + {16, 74, 71, -80, 32, 101, -47, 72, 117, -14, 0, -29, 70, 65, -12, 74}; + private static final String header = "com.google.android.vending.licensing.AESObfuscator-1|"; private Cipher mEncryptor; private Cipher mDecryptor; /** - * @param salt an array of random bytes to use for each (un)obfuscation + * @param salt an array of random bytes to use for each (un)obfuscation * @param applicationId application identifier, e.g. the package name - * @param deviceId device identifier. Use as many sources as possible to - * create this unique identifier. + * @param deviceId device identifier. Use as many sources as possible to create this unique + * identifier. */ public AESObfuscator(byte[] salt, String applicationId, String deviceId) { try { SecretKeyFactory factory = SecretKeyFactory.getInstance(KEYGEN_ALGORITHM); - KeySpec keySpec = - new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256); + KeySpec keySpec = + new PBEKeySpec((applicationId + deviceId).toCharArray(), salt, 1024, 256); SecretKey tmp = factory.generateSecret(keySpec); SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); @@ -76,9 +76,7 @@ public String obfuscate(String original, String key) { try { // Header is appended as an integrity check return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8))); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Invalid environment", e); - } catch (GeneralSecurityException e) { + } catch (UnsupportedEncodingException | GeneralSecurityException e) { throw new RuntimeException("Invalid environment", e); } } @@ -91,17 +89,13 @@ public String unobfuscate(String obfuscated, String key) throws ValidationExcept String result = new String(mDecryptor.doFinal(Base64.decode(obfuscated)), UTF8); // Check for presence of header. This serves as a final integrity check, for cases // where the block size is correct during decryption. - int headerIndex = result.indexOf(header+key); + int headerIndex = result.indexOf(header + key); if (headerIndex != 0) { throw new ValidationException("Header not found (invalid data or key)" + ":" + obfuscated); } - return result.substring(header.length()+key.length(), result.length()); - } catch (Base64DecoderException e) { - throw new ValidationException(e.getMessage() + ":" + obfuscated); - } catch (IllegalBlockSizeException e) { - throw new ValidationException(e.getMessage() + ":" + obfuscated); - } catch (BadPaddingException e) { + return result.substring(header.length() + key.length(), result.length()); + } catch (Base64DecoderException | IllegalBlockSizeException | BadPaddingException e) { throw new ValidationException(e.getMessage() + ":" + obfuscated); } catch (UnsupportedEncodingException e) { throw new RuntimeException("Invalid environment", e); diff --git a/library/src/main/java/com/google/android/vending/licensing/APKExpansionPolicy.java b/library/src/main/java/com/google/android/vending/licensing/APKExpansionPolicy.java index 17cc7a7..7441fb8 100644 --- a/library/src/main/java/com/google/android/vending/licensing/APKExpansionPolicy.java +++ b/library/src/main/java/com/google/android/vending/licensing/APKExpansionPolicy.java @@ -1,4 +1,3 @@ - package com.google.android.vending.licensing; /* @@ -17,8 +16,7 @@ * limitations under the License. */ -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; +import com.google.android.vending.licensing.util.URIQueryDecoder; import android.content.Context; import android.content.SharedPreferences; @@ -27,29 +25,31 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; /** - * Default policy. All policy decisions are based off of response data received - * from the licensing service. Specifically, the licensing server sends the - * following information: response validity period, error retry period, and - * error retry count. - *

- * These values will vary based on the the way the application is configured in - * the Android Market publishing console, such as whether the application is - * marked as free or is within its refund period, as well as how often an - * application is checking with the licensing service. - *

- * Developers who need more fine grained control over their application's - * licensing policy should implement a custom Policy. + * Default policy. All policy decisions are based off of response data received from the licensing + * service. Specifically, the licensing server sends the following information: response validity + * period, error retry period, and error retry count.

These values will vary based on the the + * way the application is configured in the Google Play publishing console, such as whether the + * application is marked as free or is within its refund period, as well as how often an application + * is checking with the licensing service.

Developers who need more fine grained control over + * their application's licensing policy should implement a custom Policy. */ public class APKExpansionPolicy implements Policy { + /** + * The design of the protocol supports n files. Currently the market can only deliver two files. + * To accommodate this, we have these two constants, but the order is the only relevant thing + * here. + */ + public static final int MAIN_FILE_URL_INDEX = 0; + public static final int PATCH_FILE_URL_INDEX = 1; private static final String TAG = "APKExpansionPolicy"; - private static final String PREFS_FILE = "com.android.vending.licensing.APKExpansionPolicy"; + private static final String PREFS_FILE = "com.google.android.vending.licensing" + + ".APKExpansionPolicy"; private static final String PREF_LAST_RESPONSE = "lastResponse"; private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; private static final String PREF_RETRY_UNTIL = "retryUntil"; @@ -59,9 +59,7 @@ public class APKExpansionPolicy implements Policy { private static final String DEFAULT_RETRY_UNTIL = "0"; private static final String DEFAULT_MAX_RETRIES = "0"; private static final String DEFAULT_RETRY_COUNT = "0"; - private static final long MILLIS_PER_MINUTE = 60 * 1000; - private long mValidityTimestamp; private long mRetryUntil; private long mMaxRetries; @@ -69,20 +67,12 @@ public class APKExpansionPolicy implements Policy { private long mLastResponseTime = 0; private int mLastResponse; private PreferenceObfuscator mPreferences; - private Vector mExpansionURLs = new Vector(); - private Vector mExpansionFileNames = new Vector(); - private Vector mExpansionFileSizes = new Vector(); + private Vector mExpansionURLs = new Vector<>(); + private Vector mExpansionFileNames = new Vector<>(); + private Vector mExpansionFileSizes = new Vector<>(); /** - * The design of the protocol supports n files. Currently the market can - * only deliver two files. To accommodate this, we have these two constants, - * but the order is the only relevant thing here. - */ - public static final int MAIN_FILE_URL_INDEX = 0; - public static final int PATCH_FILE_URL_INDEX = 1; - - /** - * @param context The context for the current application + * @param context The context for the current application * @param obfuscator An obfuscator to be used with preferences. */ public APKExpansionPolicy(Context context, Obfuscator obfuscator) { @@ -99,8 +89,8 @@ public APKExpansionPolicy(Context context, Obfuscator obfuscator) { } /** - * We call this to guarantee that we fetch a fresh policy from the server. - * This is to be used if the URL is invalid. + * We call this to guarantee that we fetch a fresh policy from the server. This is to be used if + * the URL is invalid. */ public void resetPolicy() { mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY)); @@ -112,22 +102,17 @@ public void resetPolicy() { } /** - * Process a new response from the license server. - *

- * This data will be used for computing future policy decisions. The - * following parameters are processed: - *

    - *
  • VT: the timestamp that the client should consider the response valid - * until - *
  • GT: the timestamp that the client should ignore retry errors until - *
  • GR: the number of retry errors that the client should ignore - *
- * + * Process a new response from the license server.

This data will be used for computing + * future policy decisions. The following parameters are processed:

  • VT: the timestamp + * that the client should consider the response valid until
  • GT: the timestamp that the + * client should ignore retry errors until
  • GR: the number of retry errors that the client + * should ignore
+ * * @param response the result from validating the server response - * @param rawData the raw server response data + * @param rawData the raw server response data */ public void processServerResponse(int response, - com.google.android.vending.licensing.ResponseData rawData) { + com.google.android.vending.licensing.ResponseData rawData) { // Update retry counter if (response != Policy.RETRY) { @@ -172,10 +157,9 @@ public void processServerResponse(int response, } /** - * Set the last license response received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * + * Set the last license response received from the server and add to preferences. You must + * manually call PreferenceObfuscator.commit() to commit these changes to disk. + * * @param l the response */ private void setLastResponse(int l) { @@ -184,10 +168,14 @@ private void setLastResponse(int l) { mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); } + public long getRetryCount() { + return mRetryCount; + } + /** - * Set the current retry count and add to preferences. You must manually - * call PreferenceObfuscator.commit() to commit these changes to disk. - * + * Set the current retry count and add to preferences. You must manually call + * PreferenceObfuscator.commit() to commit these changes to disk. + * * @param c the new retry count */ private void setRetryCount(long c) { @@ -195,15 +183,14 @@ private void setRetryCount(long c) { mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); } - public long getRetryCount() { - return mRetryCount; + public long getValidityTimestamp() { + return mValidityTimestamp; } /** - * Set the last validity timestamp (VT) received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * + * Set the last validity timestamp (VT) received from the server and add to preferences. You + * must manually call PreferenceObfuscator.commit() to commit these changes to disk. + * * @param validityTimestamp the VT string received */ private void setValidityTimestamp(String validityTimestamp) { @@ -221,15 +208,14 @@ private void setValidityTimestamp(String validityTimestamp) { mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); } - public long getValidityTimestamp() { - return mValidityTimestamp; + public long getRetryUntil() { + return mRetryUntil; } /** - * Set the retry until timestamp (GT) received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * + * Set the retry until timestamp (GT) received from the server and add to preferences. You must + * manually call PreferenceObfuscator.commit() to commit these changes to disk. + * * @param retryUntil the GT string received */ private void setRetryUntil(String retryUntil) { @@ -240,22 +226,21 @@ private void setRetryUntil(String retryUntil) { // No response or not parseable, expire immediately Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); retryUntil = "0"; - lRetryUntil = 0l; + lRetryUntil = 0L; } mRetryUntil = lRetryUntil; mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); } - public long getRetryUntil() { - return mRetryUntil; + public long getMaxRetries() { + return mMaxRetries; } /** - * Set the max retries value (GR) as received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. - * + * Set the max retries value (GR) as received from the server and add to preferences. You must + * manually call PreferenceObfuscator.commit() to commit these changes to disk. + * * @param maxRetries the GR string received */ private void setMaxRetries(String maxRetries) { @@ -266,22 +251,17 @@ private void setMaxRetries(String maxRetries) { // No response or not parseable, expire immediately Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); maxRetries = "0"; - lMaxRetries = 0l; + lMaxRetries = 0L; } mMaxRetries = lMaxRetries; mPreferences.putString(PREF_MAX_RETRIES, maxRetries); } - public long getMaxRetries() { - return mMaxRetries; - } - /** - * Gets the count of expansion URLs. Since expansionURLs are not committed - * to preferences, this will return zero if there has been no LVL fetch - * in the current session. - * + * Gets the count of expansion URLs. Since expansionURLs are not committed to preferences, this + * will return zero if there has been no LVL fetch in the current session. + * * @return the number of expansion URLs. (0,1,2) */ public int getExpansionURLCount() { @@ -289,13 +269,11 @@ public int getExpansionURLCount() { } /** - * Gets the expansion URL. Since these URLs are not committed to - * preferences, this will always return null if there has not been an LVL - * fetch in the current session. - * - * @param index the index of the URL to fetch. This value will be either - * MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX - * @param URL the URL to set + * Gets the expansion URL. Since these URLs are not committed to preferences, this will always + * return null if there has not been an LVL fetch in the current session. + * + * @param index the index of the URL to fetch. This value will be either MAIN_FILE_URL_INDEX or + * PATCH_FILE_URL_INDEX */ public String getExpansionURL(int index) { if (index < mExpansionURLs.size()) { @@ -305,13 +283,12 @@ public String getExpansionURL(int index) { } /** - * Sets the expansion URL. Expansion URL's are not committed to preferences, - * but are instead intended to be stored when the license response is - * processed by the front-end. - * - * @param index the index of the expansion URL. This value will be either - * MAIN_FILE_URL_INDEX or PATCH_FILE_URL_INDEX - * @param URL the URL to set + * Sets the expansion URL. Expansion URL's are not committed to preferences, but are instead + * intended to be stored when the license response is processed by the front-end. + * + * @param index the index of the expansion URL. This value will be either MAIN_FILE_URL_INDEX or + * PATCH_FILE_URL_INDEX + * @param URL the URL to set */ public void setExpansionURL(int index, String URL) { if (index >= mExpansionURLs.size()) { @@ -349,12 +326,9 @@ public void setExpansionFileSize(int index, long size) { } /** - * {@inheritDoc} This implementation allows access if either:
- *
    - *
  1. a LICENSED response was received within the validity period - *
  2. a RETRY response was received in the last minute, and we are under - * the RETRY count or in the RETRY period. - *
+ * {@inheritDoc} This implementation allows access if either:
  1. a LICENSED response + * was received within the validity period
  2. a RETRY response was received in the last minute, + * and we are under the RETRY count or in the RETRY period.
*/ public boolean allowAccess() { long ts = System.currentTimeMillis(); @@ -376,18 +350,10 @@ public boolean allowAccess() { } private Map decodeExtras(String extras) { - Map results = new HashMap(); + Map results = new HashMap<>(); try { URI rawExtras = new URI("?" + extras); - List extraList = URLEncodedUtils.parse(rawExtras, "UTF-8"); - for (NameValuePair item : extraList) { - String name = item.getName(); - int i = 0; - while (results.containsKey(name)) { - name = item.getName() + ++i; - } - results.put(name, item.getValue()); - } + URIQueryDecoder.DecodeQuery(rawExtras, results); } catch (URISyntaxException e) { Log.w(TAG, "Invalid syntax error while decoding extras data from server."); } diff --git a/library/src/main/java/com/google/android/vending/licensing/DeviceLimiter.java b/library/src/main/java/com/google/android/vending/licensing/DeviceLimiter.java index e5c5e2d..9ff3f23 100644 --- a/library/src/main/java/com/google/android/vending/licensing/DeviceLimiter.java +++ b/library/src/main/java/com/google/android/vending/licensing/DeviceLimiter.java @@ -17,23 +17,17 @@ package com.google.android.vending.licensing; /** - * Allows the developer to limit the number of devices using a single license. - *

- * The LICENSED response from the server contains a user identifier unique to - * the <application, user> pair. The developer can send this identifier - * to their own server along with some device identifier (a random number - * generated and stored once per application installation, - * {@link android.telephony.TelephonyManager#getDeviceId getDeviceId}, - * {@link android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc). - * The more sources used to identify the device, the harder it will be for an - * attacker to spoof. - *

- * The server can look at the <application, user, device id> tuple and - * restrict a user's application license to run on at most 10 different devices - * in a week (for example). We recommend not being too restrictive because a - * user might legitimately have multiple devices or be in the process of - * changing phones. This will catch egregious violations of multiple people - * sharing one license. + * Allows the developer to limit the number of devices using a single license.

The LICENSED + * response from the server contains a user identifier unique to the <application, user> pair. + * The developer can send this identifier to their own server along with some device identifier (a + * random number generated and stored once per application installation, {@link + * android.telephony.TelephonyManager#getDeviceId getDeviceId}, {@link + * android.provider.Settings.Secure#ANDROID_ID ANDROID_ID}, etc). The more sources used to identify + * the device, the harder it will be for an attacker to spoof.

The server can look at the + * <application, user, device id> tuple and restrict a user's application license to run on at + * most 10 different devices in a week (for example). We recommend not being too restrictive because + * a user might legitimately have multiple devices or be in the process of changing phones. This + * will catch egregious violations of multiple people sharing one license. */ public interface DeviceLimiter { diff --git a/library/src/main/java/com/google/android/vending/licensing/ILicenseResultListener.java b/library/src/main/java/com/google/android/vending/licensing/ILicenseResultListener.java deleted file mode 100644 index d90d6ea..0000000 --- a/library/src/main/java/com/google/android/vending/licensing/ILicenseResultListener.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * This file is auto-generated. DO NOT MODIFY. - * Original file: aidl/ILicenseResultListener.aidl - */ -package com.google.android.vending.licensing; -import java.lang.String; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.IInterface; -import android.os.Binder; -import android.os.Parcel; -public interface ILicenseResultListener extends android.os.IInterface -{ -/** Local-side IPC implementation stub class. */ -public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicenseResultListener -{ -private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicenseResultListener"; -/** Construct the stub at attach it to the interface. */ -public Stub() -{ -this.attachInterface(this, DESCRIPTOR); -} -/** - * Cast an IBinder object into an ILicenseResultListener interface, - * generating a proxy if needed. - */ -public static com.google.android.vending.licensing.ILicenseResultListener asInterface(android.os.IBinder obj) -{ -if ((obj==null)) { -return null; -} -android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); -if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicenseResultListener))) { -return ((com.google.android.vending.licensing.ILicenseResultListener)iin); -} -return new com.google.android.vending.licensing.ILicenseResultListener.Stub.Proxy(obj); -} -public android.os.IBinder asBinder() -{ -return this; -} -public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException -{ -switch (code) -{ -case INTERFACE_TRANSACTION: -{ -reply.writeString(DESCRIPTOR); -return true; -} -case TRANSACTION_verifyLicense: -{ -data.enforceInterface(DESCRIPTOR); -int _arg0; -_arg0 = data.readInt(); -java.lang.String _arg1; -_arg1 = data.readString(); -java.lang.String _arg2; -_arg2 = data.readString(); -this.verifyLicense(_arg0, _arg1, _arg2); -return true; -} -} -return super.onTransact(code, data, reply, flags); -} -private static class Proxy implements com.google.android.vending.licensing.ILicenseResultListener -{ -private android.os.IBinder mRemote; -Proxy(android.os.IBinder remote) -{ -mRemote = remote; -} -public android.os.IBinder asBinder() -{ -return mRemote; -} -public java.lang.String getInterfaceDescriptor() -{ -return DESCRIPTOR; -} -public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException -{ -android.os.Parcel _data = android.os.Parcel.obtain(); -try { -_data.writeInterfaceToken(DESCRIPTOR); -_data.writeInt(responseCode); -_data.writeString(signedData); -_data.writeString(signature); -mRemote.transact(Stub.TRANSACTION_verifyLicense, _data, null, IBinder.FLAG_ONEWAY); -} -finally { -_data.recycle(); -} -} -} -static final int TRANSACTION_verifyLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); -} -public void verifyLicense(int responseCode, java.lang.String signedData, java.lang.String signature) throws android.os.RemoteException; -} diff --git a/library/src/main/java/com/google/android/vending/licensing/ILicensingService.java b/library/src/main/java/com/google/android/vending/licensing/ILicensingService.java deleted file mode 100644 index 9559954..0000000 --- a/library/src/main/java/com/google/android/vending/licensing/ILicensingService.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * This file is auto-generated. DO NOT MODIFY. - * Original file: aidl/ILicensingService.aidl - */ -package com.google.android.vending.licensing; -import java.lang.String; -import android.os.RemoteException; -import android.os.IBinder; -import android.os.IInterface; -import android.os.Binder; -import android.os.Parcel; -public interface ILicensingService extends android.os.IInterface -{ -/** Local-side IPC implementation stub class. */ -public static abstract class Stub extends android.os.Binder implements com.google.android.vending.licensing.ILicensingService -{ -private static final java.lang.String DESCRIPTOR = "com.android.vending.licensing.ILicensingService"; -/** Construct the stub at attach it to the interface. */ -public Stub() -{ -this.attachInterface(this, DESCRIPTOR); -} -/** - * Cast an IBinder object into an ILicensingService interface, - * generating a proxy if needed. - */ -public static com.google.android.vending.licensing.ILicensingService asInterface(android.os.IBinder obj) -{ -if ((obj==null)) { -return null; -} -android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR); -if (((iin!=null)&&(iin instanceof com.google.android.vending.licensing.ILicensingService))) { -return ((com.google.android.vending.licensing.ILicensingService)iin); -} -return new com.google.android.vending.licensing.ILicensingService.Stub.Proxy(obj); -} -public android.os.IBinder asBinder() -{ -return this; -} -public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException -{ -switch (code) -{ -case INTERFACE_TRANSACTION: -{ -reply.writeString(DESCRIPTOR); -return true; -} -case TRANSACTION_checkLicense: -{ -data.enforceInterface(DESCRIPTOR); -long _arg0; -_arg0 = data.readLong(); -java.lang.String _arg1; -_arg1 = data.readString(); -com.google.android.vending.licensing.ILicenseResultListener _arg2; -_arg2 = com.google.android.vending.licensing.ILicenseResultListener.Stub.asInterface(data.readStrongBinder()); -this.checkLicense(_arg0, _arg1, _arg2); -return true; -} -} -return super.onTransact(code, data, reply, flags); -} -private static class Proxy implements com.google.android.vending.licensing.ILicensingService -{ -private android.os.IBinder mRemote; -Proxy(android.os.IBinder remote) -{ -mRemote = remote; -} -public android.os.IBinder asBinder() -{ -return mRemote; -} -public java.lang.String getInterfaceDescriptor() -{ -return DESCRIPTOR; -} -public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException -{ -android.os.Parcel _data = android.os.Parcel.obtain(); -try { -_data.writeInterfaceToken(DESCRIPTOR); -_data.writeLong(nonce); -_data.writeString(packageName); -_data.writeStrongBinder((((listener!=null))?(listener.asBinder()):(null))); -mRemote.transact(Stub.TRANSACTION_checkLicense, _data, null, IBinder.FLAG_ONEWAY); -} -finally { -_data.recycle(); -} -} -} -static final int TRANSACTION_checkLicense = (IBinder.FIRST_CALL_TRANSACTION + 0); -} -public void checkLicense(long nonce, java.lang.String packageName, com.google.android.vending.licensing.ILicenseResultListener listener) throws android.os.RemoteException; -} diff --git a/library/src/main/java/com/google/android/vending/licensing/LicenseChecker.java b/library/src/main/java/com/google/android/vending/licensing/LicenseChecker.java index 621f8d8..9221f67 100644 --- a/library/src/main/java/com/google/android/vending/licensing/LicenseChecker.java +++ b/library/src/main/java/com/google/android/vending/licensing/LicenseChecker.java @@ -19,6 +19,7 @@ import com.google.android.vending.licensing.util.Base64; import com.google.android.vending.licensing.util.Base64DecoderException; +import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -31,12 +32,16 @@ import android.provider.Settings.Secure; import android.util.Log; +import com.android.vending.licensing.ILicenseResultListener; +import com.android.vending.licensing.ILicensingService; + import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashSet; import java.util.LinkedList; @@ -44,16 +49,14 @@ import java.util.Set; /** - * Client library for Android Market license verifications. - *

- * The LicenseChecker is configured via a {@link Policy} which contains the - * logic to determine whether a user should have access to the application. For - * example, the Policy can define a threshold for allowable number of server or - * client failures before the library reports the user as not having access. - *

- * Must also provide the Base64-encoded RSA public key associated with your - * developer account. The public key is obtainable from the publisher site. + * Client library for Google Play license verifications.

The LicenseChecker is configured via a + * {@link Policy} which contains the logic to determine whether a user should have access to the + * application. For example, the Policy can define a threshold for allowable number of server or + * client failures before the library reports the user as not having access.

Must also provide + * the Base64-encoded RSA public key associated with your developer account. The public key is + * obtainable from the publisher site. */ +@SuppressLint({"SimpleDateFormat", "HardwareIds"}) public class LicenseChecker implements ServiceConnection { private static final String TAG = "LicenseChecker"; @@ -64,25 +67,23 @@ public class LicenseChecker implements ServiceConnection { private static final SecureRandom RANDOM = new SecureRandom(); private static final boolean DEBUG_LICENSE_ERROR = false; - - private ILicensingService mService; - - private PublicKey mPublicKey; private final Context mContext; private final Policy mPolicy; + private final String mPackageName; + private final String mVersionCode; + private final Set mChecksInProgress = new HashSet<>(); + private final Queue mPendingChecks = new LinkedList<>(); + private ILicensingService mService; + private PublicKey mPublicKey; /** - * A handler for running tasks on a background thread. We don't want license - * processing to block the UI thread. + * A handler for running tasks on a background thread. We don't want license processing to block + * the UI thread. */ private Handler mHandler; - private final String mPackageName; - private final String mVersionCode; - private final Set mChecksInProgress = new HashSet(); - private final Queue mPendingChecks = new LinkedList(); /** - * @param context a Context - * @param policy implementation of Policy + * @param context a Context + * @param policy implementation of Policy * @param encodedPublicKey Base64-encoded RSA public key * @throws IllegalArgumentException if encodedPublicKey is invalid */ @@ -98,9 +99,8 @@ public LicenseChecker(Context context, Policy policy, String encodedPublicKey) { } /** - * Generates a PublicKey instance from a string containing the - * Base64-encoded public key. - * + * Generates a PublicKey instance from a string containing the Base64-encoded public key. + * * @param encodedPublicKey Base64-encoded public key * @throws IllegalArgumentException if encodedPublicKey is invalid */ @@ -123,15 +123,26 @@ private static PublicKey generatePublicKey(String encodedPublicKey) { } /** - * Checks if the user should have access to the app. Binds the service if necessary. - *

- * NOTE: This call uses a trivially obfuscated string (base64-encoded). For best security, - * we recommend obfuscating the string that is passed into bindService using another method - * of your own devising. - *

- * source string: "com.android.vending.licensing.ILicensingService" - *

- * @param callback + * Get version code for the application package name. + * + * @param packageName application package name + * @return the version code or empty string if package not found + */ + private static String getVersionCode(Context context, String packageName) { + try { + return String.valueOf( + context.getPackageManager().getPackageInfo(packageName, 0).versionCode); + } catch (NameNotFoundException e) { + Log.e(TAG, "Package not found. could not get version code."); + return ""; + } + } + + /** + * Checks if the user should have access to the app. Binds the service if necessary.

NOTE: + * This call uses a trivially obfuscated string (base64-encoded). For best security, we + * recommend obfuscating the string that is passed into bindService using another method of your + * own devising.

source string: "com.android.vending.licensing.ILicensingService"

*/ public synchronized void checkAccess(LicenseCheckerCallback callback) { // If we have a valid recent LICENSED response, we can skip asking @@ -150,11 +161,32 @@ public synchronized void checkAccess(LicenseCheckerCallback callback) { .bindService( new Intent( new String( - Base64.decode("Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))) - .setPackage("com.android.vending"), + // Base64 encoded - + // com.android.vending.licensing + // .ILicensingService + // Consider encoding this in another way in your + // code to improve security + Base64.decode( + "Y29tLmFuZHJvaWQudmVuZGluZy5saWNlbnNpbmcuSUxpY2Vuc2luZ1NlcnZpY2U="))) + // As of Android 5.0, implicit + // Service Intents are no longer + // allowed because it's not + // possible for the user to + // participate in disambiguating + // them. This does mean we break + // compatibility with Android + // Cupcake devices with this + // release, since setPackage was + // added in Donut. + .setPackage( + new String( + // Base64 + // encoded - + // com.android.vending + Base64.decode( + "Y29tLmFuZHJvaWQudmVuZGluZw=="))), this, // ServiceConnection. Context.BIND_AUTO_CREATE); - if (bindResult) { mPendingChecks.offer(validator); } else { @@ -196,7 +228,71 @@ private synchronized void finishCheck(LicenseValidator validator) { } } + public synchronized void onServiceConnected(ComponentName name, IBinder service) { + mService = ILicensingService.Stub.asInterface(service); + runChecks(); + } + + public synchronized void onServiceDisconnected(ComponentName name) { + // Called when the connection with the service has been + // unexpectedly disconnected. That is, Market crashed. + // If there are any checks in progress, the timeouts will handle them. + Log.w(TAG, "Service unexpectedly disconnected."); + mService = null; + } + + /** + * Generates policy response for service connection errors, as a result of disconnections or + * timeouts. + */ + private synchronized void handleServiceConnectionError(LicenseValidator validator) { + mPolicy.processServerResponse(Policy.RETRY, null); + + if (mPolicy.allowAccess()) { + validator.getCallback().allow(Policy.RETRY); + } else { + validator.getCallback().dontAllow(Policy.RETRY); + } + } + + /** + * Unbinds service if necessary and removes reference to it. + */ + private void cleanupService() { + if (mService != null) { + try { + mContext.unbindService(this); + } catch (IllegalArgumentException e) { + // Somehow we've already been unbound. This is a non-fatal + // error. + Log.e(TAG, "Unable to unbind from licensing service (already unbound)"); + } + mService = null; + } + } + + /** + * Inform the library that the context is about to be destroyed, so that any open connections + * can be cleaned up.

Failure to call this method can result in a crash under certain + * circumstances, such as during screen rotation if an Activity requests the license check or + * when the user exits the application. + */ + public synchronized void onDestroy() { + cleanupService(); + mHandler.getLooper().quit(); + } + + /** + * Generates a nonce (number used once). + */ + private int generateNonce() { + return RANDOM.nextInt(); + } + private class ResultListener extends ILicenseResultListener.Stub { + private static final int ERROR_CONTACTING_SERVER = 0x101; + private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; + private static final int ERROR_NON_MATCHING_UID = 0x103; private final LicenseValidator mValidator; private Runnable mOnTimeout; @@ -212,14 +308,10 @@ public void run() { startTimeout(); } - private static final int ERROR_CONTACTING_SERVER = 0x101; - private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; - private static final int ERROR_NON_MATCHING_UID = 0x103; - // Runs in IPC thread pool. Post it to the Handler, so we can guarantee // either this or the timeout runs. public void verifyLicense(final int responseCode, final String signedData, - final String signature) { + final String signature) { mHandler.post(new Runnable() { public void run() { Log.i(TAG, "Received response."); @@ -255,7 +347,9 @@ public void run() { Date date = new Date(); Log.d(TAG, "Server Failure: " + stringError); Log.d(TAG, "Android ID: " + android_id); - Log.d(TAG, "Time: " + date.toGMTString()); + SimpleDateFormat df = new SimpleDateFormat("dd MMM yyyy HH:mm:ss"); + String asGmt = df.format(date) + " GMT"; + Log.d(TAG, "Time: " + asGmt); } } @@ -273,80 +367,4 @@ private void clearTimeout() { mHandler.removeCallbacks(mOnTimeout); } } - - public synchronized void onServiceConnected(ComponentName name, IBinder service) { - mService = ILicensingService.Stub.asInterface(service); - runChecks(); - } - - public synchronized void onServiceDisconnected(ComponentName name) { - // Called when the connection with the service has been - // unexpectedly disconnected. That is, Market crashed. - // If there are any checks in progress, the timeouts will handle them. - Log.w(TAG, "Service unexpectedly disconnected."); - mService = null; - } - - /** - * Generates policy response for service connection errors, as a result of - * disconnections or timeouts. - */ - private synchronized void handleServiceConnectionError(LicenseValidator validator) { - mPolicy.processServerResponse(Policy.RETRY, null); - - if (mPolicy.allowAccess()) { - validator.getCallback().allow(Policy.RETRY); - } else { - validator.getCallback().dontAllow(Policy.RETRY); - } - } - - /** Unbinds service if necessary and removes reference to it. */ - private void cleanupService() { - if (mService != null) { - try { - mContext.unbindService(this); - } catch (IllegalArgumentException e) { - // Somehow we've already been unbound. This is a non-fatal - // error. - Log.e(TAG, "Unable to unbind from licensing service (already unbound)"); - } - mService = null; - } - } - - /** - * Inform the library that the context is about to be destroyed, so that any - * open connections can be cleaned up. - *

- * Failure to call this method can result in a crash under certain - * circumstances, such as during screen rotation if an Activity requests the - * license check or when the user exits the application. - */ - public synchronized void onDestroy() { - cleanupService(); - mHandler.getLooper().quit(); - } - - /** Generates a nonce (number used once). */ - private int generateNonce() { - return RANDOM.nextInt(); - } - - /** - * Get version code for the application package name. - * - * @param context - * @param packageName application package name - * @return the version code or empty string if package not found - */ - private static String getVersionCode(Context context, String packageName) { - try { - return String.valueOf(context.getPackageManager().getPackageInfo(packageName, 0). - versionCode); - } catch (NameNotFoundException e) { - Log.e(TAG, "Package not found. could not get version code."); - return ""; - } - } } diff --git a/library/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java b/library/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java index b250a71..08d2b61 100644 --- a/library/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java +++ b/library/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java @@ -17,51 +17,47 @@ package com.google.android.vending.licensing; /** - * Callback for the license checker library. - *

- * Upon checking with the Market server and conferring with the {@link Policy}, - * the library calls the appropriate callback method to communicate the result. - *

- * The callback does not occur in the original checking thread. Your - * application should post to the appropriate handling thread or lock - * accordingly. - *

- * The reason that is passed back with allow/dontAllow is the base status handed - * to the policy for allowed/disallowing the license. Policy.RETRY will call - * allow or dontAllow depending on other statistics associated with the policy, - * while in most cases Policy.NOT_LICENSED will call dontAllow and - * Policy.LICENSED will Allow. + * Callback for the license checker library.

Upon checking with the Market server and conferring + * with the {@link Policy}, the library calls the appropriate callback method to communicate the + * result.

The callback does not occur in the original checking thread. Your application + * should post to the appropriate handling thread or lock accordingly.

The reason that is passed + * back with allow/dontAllow is the base status handed to the policy for allowed/disallowing the + * license. Policy.RETRY will call allow or dontAllow depending on other statistics associated with + * the policy, while in most cases Policy.NOT_LICENSED will call dontAllow and Policy.LICENSED will + * Allow. */ public interface LicenseCheckerCallback { + /** + * Application error codes. + */ + int ERROR_INVALID_PACKAGE_NAME = 1; + int ERROR_NON_MATCHING_UID = 2; + int ERROR_NOT_MARKET_MANAGED = 3; + int ERROR_CHECK_IN_PROGRESS = 4; + int ERROR_INVALID_PUBLIC_KEY = 5; + int ERROR_MISSING_PERMISSION = 6; + /** * Allow use. App should proceed as normal. - * - * @param reason Policy.LICENSED or Policy.RETRY typically. (although in - * theory the policy can return Policy.NOT_LICENSED here as well) + * + * @param reason Policy.LICENSED or Policy.RETRY typically. (although in theory the policy can + * return Policy.NOT_LICENSED here as well) */ - public void allow(int reason); + void allow(int reason); /** * Don't allow use. App should inform user and take appropriate action. - * - * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory - * the policy can return Policy.LICENSED here as well --- - * perhaps the call to the LVL took too long, for example) + * + * @param reason Policy.NOT_LICENSED or Policy.RETRY. (although in theory the policy can return + * Policy.LICENSED here as well --- perhaps the call to the LVL took too long, for + * example) */ - public void dontAllow(int reason); - - /** Application error codes. */ - public static final int ERROR_INVALID_PACKAGE_NAME = 1; - public static final int ERROR_NON_MATCHING_UID = 2; - public static final int ERROR_NOT_MARKET_MANAGED = 3; - public static final int ERROR_CHECK_IN_PROGRESS = 4; - public static final int ERROR_INVALID_PUBLIC_KEY = 5; - public static final int ERROR_MISSING_PERMISSION = 6; + void dontAllow(int reason); /** - * Error in application code. Caller did not call or set up license checker - * correctly. Should be considered fatal. + * Error in application code. Caller did not call or set up license checker correctly. Should be + * considered fatal. */ - public void applicationError(int errorCode); + void applicationError(int errorCode); } diff --git a/library/src/main/java/com/google/android/vending/licensing/LicenseValidator.java b/library/src/main/java/com/google/android/vending/licensing/LicenseValidator.java index 5336305..9948e09 100644 --- a/library/src/main/java/com/google/android/vending/licensing/LicenseValidator.java +++ b/library/src/main/java/com/google/android/vending/licensing/LicenseValidator.java @@ -29,8 +29,7 @@ import java.security.SignatureException; /** - * Contains data related to a licensing request and methods to verify - * and process the response. + * Contains data related to a licensing request and methods to verify and process the response. */ class LicenseValidator { private static final String TAG = "LicenseValidator"; @@ -46,7 +45,7 @@ class LicenseValidator { private static final int ERROR_CONTACTING_SERVER = 0x101; private static final int ERROR_INVALID_PACKAGE_NAME = 0x102; private static final int ERROR_NON_MATCHING_UID = 0x103; - + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; private final Policy mPolicy; private final LicenseCheckerCallback mCallback; private final int mNonce; @@ -55,7 +54,7 @@ class LicenseValidator { private final DeviceLimiter mDeviceLimiter; LicenseValidator(Policy policy, DeviceLimiter deviceLimiter, LicenseCheckerCallback callback, - int nonce, String packageName, String versionCode) { + int nonce, String packageName, String versionCode) { mPolicy = policy; mDeviceLimiter = deviceLimiter; mCallback = callback; @@ -76,15 +75,13 @@ public String getPackageName() { return mPackageName; } - private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; - /** * Verifies the response from server and calls appropriate callback method. * - * @param publicKey public key associated with the developer account + * @param publicKey public key associated with the developer account * @param responseCode server response code - * @param signedData signed data from server - * @param signature server signature + * @param signedData signed data from server + * @param signature server signature */ public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) { String userId = null; @@ -92,12 +89,15 @@ public void verify(PublicKey publicKey, int responseCode, String signedData, Str ResponseData data = null; if (responseCode == LICENSED || responseCode == NOT_LICENSED || responseCode == LICENSED_OLD_KEY) { - if (signedData == null) { - handleInvalidResponse(); - return; - } // Verify signature. try { + if (TextUtils.isEmpty(signedData)) { + Log.e(TAG, "Signature verification failed: signedData is empty. " + + "(Device not signed-in to any Google accounts?)"); + handleInvalidResponse(); + return; + } + Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM); sig.initVerify(publicKey); sig.update(signedData.getBytes()); @@ -107,14 +107,12 @@ public void verify(PublicKey publicKey, int responseCode, String signedData, Str handleInvalidResponse(); return; } - } catch (NoSuchAlgorithmException e) { + } catch (NoSuchAlgorithmException | SignatureException e) { // This can't happen on an Android compatible device. throw new RuntimeException(e); } catch (InvalidKeyException e) { handleApplicationError(LicenseCheckerCallback.ERROR_INVALID_PUBLIC_KEY); return; - } catch (SignatureException e) { - throw new RuntimeException(e); } catch (Base64DecoderException e) { Log.e(TAG, "Could not Base64-decode signature."); handleInvalidResponse(); @@ -201,9 +199,6 @@ public void verify(PublicKey publicKey, int responseCode, String signedData, Str /** * Confers with policy and calls appropriate callback method. - * - * @param response - * @param rawData */ private void handleResponse(int response, ResponseData rawData) { // Update policy data and increment retry counter (if needed) diff --git a/library/src/main/java/com/google/android/vending/licensing/NullDeviceLimiter.java b/library/src/main/java/com/google/android/vending/licensing/NullDeviceLimiter.java index d87af31..ae49cc9 100644 --- a/library/src/main/java/com/google/android/vending/licensing/NullDeviceLimiter.java +++ b/library/src/main/java/com/google/android/vending/licensing/NullDeviceLimiter.java @@ -17,12 +17,10 @@ package com.google.android.vending.licensing; /** - * A DeviceLimiter that doesn't limit the number of devices that can use a - * given user's license. - *

- * Unless you have reason to believe that your application is being pirated - * by multiple users using the same license (signing in to Market as the same - * user), we recommend you use this implementation. + * A DeviceLimiter that doesn't limit the number of devices that can use a given user's license.

+ * Unless you have reason to believe that your application is being pirated by multiple users using + * the same license (signing in to Market as the same user), we recommend you use this + * implementation. */ public class NullDeviceLimiter implements DeviceLimiter { diff --git a/library/src/main/java/com/google/android/vending/licensing/Obfuscator.java b/library/src/main/java/com/google/android/vending/licensing/Obfuscator.java index b5d510d..37d4fbd 100644 --- a/library/src/main/java/com/google/android/vending/licensing/Obfuscator.java +++ b/library/src/main/java/com/google/android/vending/licensing/Obfuscator.java @@ -17,13 +17,12 @@ package com.google.android.vending.licensing; /** - * Interface used as part of a {@link Policy} to allow application authors to obfuscate - * licensing data that will be stored into a SharedPreferences file. - *

- * Any transformation scheme must be reversable. Implementing classes may optionally implement an - * integrity check to further prevent modification to preference data. Implementing classes - * should use device-specific information as a key in the obfuscation algorithm to prevent - * obfuscated preferences from being shared among devices. + * Interface used as part of a {@link Policy} to allow application authors to obfuscate licensing + * data that will be stored into a SharedPreferences file.

Any transformation scheme must be + * reversable. Implementing classes may optionally implement an integrity check to further prevent + * modification to preference data. Implementing classes should use device-specific information as a + * key in the obfuscation algorithm to prevent obfuscated preferences from being shared among + * devices. */ public interface Obfuscator { @@ -31,7 +30,7 @@ public interface Obfuscator { * Obfuscate a string that is being stored into shared preferences. * * @param original The data that is to be obfuscated. - * @param key The key for the data that is to be obfuscated. + * @param key The key for the data that is to be obfuscated. * @return A transformed version of the original data. */ String obfuscate(String original, String key); @@ -39,9 +38,9 @@ public interface Obfuscator { /** * Undo the transformation applied to data by the obfuscate() method. * - * @param original The data that is to be obfuscated. - * @param key The key for the data that is to be obfuscated. - * @return A transformed version of the original data. + * @param obfuscated The data that is to be un-obfuscated. + * @param key The key for the data that is to be obfuscated. + * @return The original data transformed by the obfuscate() method. * @throws ValidationException Optionally thrown if a data integrity check fails. */ String unobfuscate(String obfuscated, String key) throws ValidationException; diff --git a/library/src/main/java/com/google/android/vending/licensing/Policy.java b/library/src/main/java/com/google/android/vending/licensing/Policy.java index fa267fc..afdee76 100644 --- a/library/src/main/java/com/google/android/vending/licensing/Policy.java +++ b/library/src/main/java/com/google/android/vending/licensing/Policy.java @@ -17,8 +17,8 @@ package com.google.android.vending.licensing; /** - * Policy used by {@link LicenseChecker} to determine whether a user should have - * access to the application. + * Policy used by {@link LicenseChecker} to determine whether a user should have access to the + * application. */ public interface Policy { @@ -30,25 +30,24 @@ public interface Policy { /** * LICENSED means that the server returned back a valid license response */ - public static final int LICENSED = 0x0100; + int LICENSED = 0x0100; /** - * NOT_LICENSED means that the server returned back a valid license response - * that indicated that the user definitively is not licensed + * NOT_LICENSED means that the server returned back a valid license response that indicated that + * the user definitively is not licensed */ - public static final int NOT_LICENSED = 0x0231; + int NOT_LICENSED = 0x0231; /** - * RETRY means that the license response was unable to be determined --- - * perhaps as a result of faulty networking + * RETRY means that the license response was unable to be determined --- perhaps as a result of + * faulty networking */ - public static final int RETRY = 0x0123; + int RETRY = 0x0123; /** - * Provide results from contact with the license server. Retry counts are - * incremented if the current value of response is RETRY. Results will be - * used for any future policy decisions. - * + * Provide results from contact with the license server. Retry counts are incremented if the + * current value of response is RETRY. Results will be used for any future policy decisions. + * * @param response the result from validating the server response - * @param rawData the raw server response data, can be null for RETRY + * @param rawData the raw server response data, can be null for RETRY */ void processServerResponse(int response, ResponseData rawData); diff --git a/library/src/main/java/com/google/android/vending/licensing/PreferenceObfuscator.java b/library/src/main/java/com/google/android/vending/licensing/PreferenceObfuscator.java index 7c42bfc..a075e3f 100644 --- a/library/src/main/java/com/google/android/vending/licensing/PreferenceObfuscator.java +++ b/library/src/main/java/com/google/android/vending/licensing/PreferenceObfuscator.java @@ -16,6 +16,7 @@ package com.google.android.vending.licensing; +import android.annotation.SuppressLint; import android.content.SharedPreferences; import android.util.Log; @@ -34,7 +35,7 @@ public class PreferenceObfuscator { * Constructor. * * @param sp A SharedPreferences instance provided by the system. - * @param o The Obfuscator to use when reading or writing data. + * @param o The Obfuscator to use when reading or writing data. */ public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) { mPreferences = sp; @@ -42,6 +43,7 @@ public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) { mEditor = null; } + @SuppressLint("CommitPrefEdits") public void putString(String key, String value) { if (mEditor == null) { mEditor = mPreferences.edit(); diff --git a/library/src/main/java/com/google/android/vending/licensing/ResponseData.java b/library/src/main/java/com/google/android/vending/licensing/ResponseData.java index 2adef37..8841284 100644 --- a/library/src/main/java/com/google/android/vending/licensing/ResponseData.java +++ b/library/src/main/java/com/google/android/vending/licensing/ResponseData.java @@ -16,10 +16,10 @@ package com.google.android.vending.licensing; -import java.util.regex.Pattern; - import android.text.TextUtils; +import java.util.regex.Pattern; + /** * ResponseData from licensing server. */ @@ -31,29 +31,31 @@ public class ResponseData { public String versionCode; public String userId; public long timestamp; - /** Response-specific data. */ + /** + * Response-specific data. + */ public String extra; /** * Parses response string into ResponseData. * * @param responseData response data string - * @throws IllegalArgumentException upon parsing error * @return ResponseData object + * @throws IllegalArgumentException upon parsing error */ public static ResponseData parse(String responseData) { // Must parse out main response data and response-specific data. - int index = responseData.indexOf(':'); - String mainData, extraData; - if ( -1 == index ) { - mainData = responseData; - extraData = ""; - } else { - mainData = responseData.substring(0, index); - extraData = index >= responseData.length() ? "" : responseData.substring(index+1); - } + int index = responseData.indexOf(':'); + String mainData, extraData; + if (-1 == index) { + mainData = responseData; + extraData = ""; + } else { + mainData = responseData.substring(0, index); + extraData = index >= responseData.length() ? "" : responseData.substring(index + 1); + } - String [] fields = TextUtils.split(mainData, Pattern.quote("|")); + String[] fields = TextUtils.split(mainData, Pattern.quote("|")); if (fields.length < 6) { throw new IllegalArgumentException("Wrong number of fields."); } @@ -73,7 +75,9 @@ public static ResponseData parse(String responseData) { @Override public String toString() { - return TextUtils.join("|", new Object [] { responseCode, nonce, packageName, versionCode, - userId, timestamp }); + return TextUtils.join("|", new Object[]{ + responseCode, nonce, packageName, versionCode, + userId, timestamp + }); } } diff --git a/library/src/main/java/com/google/android/vending/licensing/ServerManagedPolicy.java b/library/src/main/java/com/google/android/vending/licensing/ServerManagedPolicy.java index fbf8cf6..2f290b0 100644 --- a/library/src/main/java/com/google/android/vending/licensing/ServerManagedPolicy.java +++ b/library/src/main/java/com/google/android/vending/licensing/ServerManagedPolicy.java @@ -16,37 +16,31 @@ package com.google.android.vending.licensing; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; +import com.google.android.vending.licensing.util.URIQueryDecoder; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; -import java.util.List; import java.util.Map; -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - /** - * Default policy. All policy decisions are based off of response data received - * from the licensing service. Specifically, the licensing server sends the - * following information: response validity period, error retry period, and - * error retry count. - *

- * These values will vary based on the the way the application is configured in - * the Android Market publishing console, such as whether the application is - * marked as free or is within its refund period, as well as how often an - * application is checking with the licensing service. - *

- * Developers who need more fine grained control over their application's - * licensing policy should implement a custom Policy. + * Default policy. All policy decisions are based off of response data received from the licensing + * service. Specifically, the licensing server sends the following information: response validity + * period, error retry period, and error retry count.

These values will vary based on the the + * way the application is configured in the Google Play publishing console, such as whether the + * application is marked as free or is within its refund period, as well as how often an application + * is checking with the licensing service.

Developers who need more fine grained control over + * their application's licensing policy should implement a custom Policy. */ public class ServerManagedPolicy implements Policy { private static final String TAG = "ServerManagedPolicy"; - private static final String PREFS_FILE = "com.android.vending.licensing.ServerManagedPolicy"; + private static final String PREFS_FILE = "com.google.android.vending.licensing" + + ".ServerManagedPolicy"; private static final String PREF_LAST_RESPONSE = "lastResponse"; private static final String PREF_VALIDITY_TIMESTAMP = "validityTimestamp"; private static final String PREF_RETRY_UNTIL = "retryUntil"; @@ -68,7 +62,7 @@ public class ServerManagedPolicy implements Policy { private PreferenceObfuscator mPreferences; /** - * @param context The context for the current application + * @param context The context for the current application * @param obfuscator An obfuscator to be used with preferences. */ public ServerManagedPolicy(Context context, Obfuscator obfuscator) { @@ -76,7 +70,7 @@ public ServerManagedPolicy(Context context, Obfuscator obfuscator) { SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); mPreferences = new PreferenceObfuscator(sp, obfuscator); mLastResponse = Integer.parseInt( - mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); + mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(Policy.RETRY))); mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, DEFAULT_VALIDITY_TIMESTAMP)); mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); @@ -85,19 +79,14 @@ public ServerManagedPolicy(Context context, Obfuscator obfuscator) { } /** - * Process a new response from the license server. - *

- * This data will be used for computing future policy decisions. The - * following parameters are processed: - *

    - *
  • VT: the timestamp that the client should consider the response - * valid until - *
  • GT: the timestamp that the client should ignore retry errors until - *
  • GR: the number of retry errors that the client should ignore - *
+ * Process a new response from the license server.

This data will be used for computing + * future policy decisions. The following parameters are processed:

  • VT: the timestamp + * that the client should consider the response valid until
  • GT: the timestamp that the + * client should ignore retry errors until
  • GR: the number of retry errors that the client + * should ignore
* * @param response the result from validating the server response - * @param rawData the raw server response data + * @param rawData the raw server response data */ public void processServerResponse(int response, ResponseData rawData) { @@ -127,9 +116,8 @@ public void processServerResponse(int response, ResponseData rawData) { } /** - * Set the last license response received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. + * Set the last license response received from the server and add to preferences. You must + * manually call PreferenceObfuscator.commit() to commit these changes to disk. * * @param l the response */ @@ -139,9 +127,13 @@ private void setLastResponse(int l) { mPreferences.putString(PREF_LAST_RESPONSE, Integer.toString(l)); } + public long getRetryCount() { + return mRetryCount; + } + /** - * Set the current retry count and add to preferences. You must manually - * call PreferenceObfuscator.commit() to commit these changes to disk. + * Set the current retry count and add to preferences. You must manually call + * PreferenceObfuscator.commit() to commit these changes to disk. * * @param c the new retry count */ @@ -150,14 +142,13 @@ private void setRetryCount(long c) { mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); } - public long getRetryCount() { - return mRetryCount; + public long getValidityTimestamp() { + return mValidityTimestamp; } /** - * Set the last validity timestamp (VT) received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. + * Set the last validity timestamp (VT) received from the server and add to preferences. You + * must manually call PreferenceObfuscator.commit() to commit these changes to disk. * * @param validityTimestamp the VT string received */ @@ -176,14 +167,13 @@ private void setValidityTimestamp(String validityTimestamp) { mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); } - public long getValidityTimestamp() { - return mValidityTimestamp; + public long getRetryUntil() { + return mRetryUntil; } /** - * Set the retry until timestamp (GT) received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. + * Set the retry until timestamp (GT) received from the server and add to preferences. You must + * manually call PreferenceObfuscator.commit() to commit these changes to disk. * * @param retryUntil the GT string received */ @@ -195,21 +185,20 @@ private void setRetryUntil(String retryUntil) { // No response or not parsable, expire immediately Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); retryUntil = "0"; - lRetryUntil = 0l; + lRetryUntil = 0L; } mRetryUntil = lRetryUntil; mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); } - public long getRetryUntil() { - return mRetryUntil; + public long getMaxRetries() { + return mMaxRetries; } /** - * Set the max retries value (GR) as received from the server and add to - * preferences. You must manually call PreferenceObfuscator.commit() to - * commit these changes to disk. + * Set the max retries value (GR) as received from the server and add to preferences. You must + * manually call PreferenceObfuscator.commit() to commit these changes to disk. * * @param maxRetries the GR string received */ @@ -221,26 +210,19 @@ private void setMaxRetries(String maxRetries) { // No response or not parsable, expire immediately Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); maxRetries = "0"; - lMaxRetries = 0l; + lMaxRetries = 0L; } mMaxRetries = lMaxRetries; mPreferences.putString(PREF_MAX_RETRIES, maxRetries); } - public long getMaxRetries() { - return mMaxRetries; - } - /** * {@inheritDoc} * - * This implementation allows access if either:
- *
    - *
  1. a LICENSED response was received within the validity period - *
  2. a RETRY response was received in the last minute, and we are under - * the RETRY count or in the RETRY period. - *
+ * This implementation allows access if either:
  1. a LICENSED response was received + * within the validity period
  2. a RETRY response was received in the last minute, and we are + * under the RETRY count or in the RETRY period.
*/ public boolean allowAccess() { long ts = System.currentTimeMillis(); @@ -251,7 +233,7 @@ public boolean allowAccess() { return true; } } else if (mLastResponse == Policy.RETRY && - ts < mLastResponseTime + MILLIS_PER_MINUTE) { + ts < mLastResponseTime + MILLIS_PER_MINUTE) { // Only allow access if we are within the retry period or we haven't used up our // max retries. return (ts <= mRetryUntil || mRetryCount <= mMaxRetries); @@ -260,15 +242,12 @@ public boolean allowAccess() { } private Map decodeExtras(String extras) { - Map results = new HashMap(); + Map results = new HashMap<>(); try { URI rawExtras = new URI("?" + extras); - List extraList = URLEncodedUtils.parse(rawExtras, "UTF-8"); - for (NameValuePair item : extraList) { - results.put(item.getName(), item.getValue()); - } + URIQueryDecoder.DecodeQuery(rawExtras, results); } catch (URISyntaxException e) { - Log.w(TAG, "Invalid syntax error while decoding extras data from server."); + Log.w(TAG, "Invalid syntax error while decoding extras data from server."); } return results; } diff --git a/library/src/main/java/com/google/android/vending/licensing/StrictPolicy.java b/library/src/main/java/com/google/android/vending/licensing/StrictPolicy.java index d8d83b4..2501837 100644 --- a/library/src/main/java/com/google/android/vending/licensing/StrictPolicy.java +++ b/library/src/main/java/com/google/android/vending/licensing/StrictPolicy.java @@ -17,17 +17,12 @@ package com.google.android.vending.licensing; /** - * Non-caching policy. All requests will be sent to the licensing service, - * and no local caching is performed. - *

- * Using a non-caching policy ensures that there is no local preference data - * for malicious users to tamper with. As a side effect, applications - * will not be permitted to run while offline. Developers should carefully - * weigh the risks of using this Policy over one which implements caching, - * such as ServerManagedPolicy. - *

- * Access to the application is only allowed if a LICESNED response is. - * received. All other responses (including RETRY) will deny access. + * Non-caching policy. All requests will be sent to the licensing service, and no local caching is + * performed.

Using a non-caching policy ensures that there is no local preference data for + * malicious users to tamper with. As a side effect, applications will not be permitted to run while + * offline. Developers should carefully weigh the risks of using this Policy over one which + * implements caching, such as ServerManagedPolicy.

Access to the application is only allowed if + * a LICESNED response is. received. All other responses (including RETRY) will deny access. */ public class StrictPolicy implements Policy { @@ -39,12 +34,11 @@ public StrictPolicy() { } /** - * Process a new response from the license server. Since we aren't - * performing any caching, this equates to reading the LicenseResponse. - * Any ResponseData provided is ignored. + * Process a new response from the license server. Since we aren't performing any caching, this + * equates to reading the LicenseResponse. Any ResponseData provided is ignored. * * @param response the result from validating the server response - * @param rawData the raw server response data + * @param rawData the raw server response data */ public void processServerResponse(int response, ResponseData rawData) { mLastResponse = response; @@ -53,8 +47,8 @@ public void processServerResponse(int response, ResponseData rawData) { /** * {@inheritDoc} * - * This implementation allows access if and only if a LICENSED response - * was received the last time the server was contacted. + * This implementation allows access if and only if a LICENSED response was received the last + * time the server was contacted. */ public boolean allowAccess() { return (mLastResponse == Policy.LICENSED); diff --git a/library/src/main/java/com/google/android/vending/licensing/ValidationException.java b/library/src/main/java/com/google/android/vending/licensing/ValidationException.java index ee4df47..3d49e58 100644 --- a/library/src/main/java/com/google/android/vending/licensing/ValidationException.java +++ b/library/src/main/java/com/google/android/vending/licensing/ValidationException.java @@ -17,17 +17,17 @@ package com.google.android.vending.licensing; /** - * Indicates that an error occurred while validating the integrity of data managed by an - * {@link Obfuscator}.} + * Indicates that an error occurred while validating the integrity of data managed by an {@link + * Obfuscator}.} */ public class ValidationException extends Exception { + private static final long serialVersionUID = 1L; + public ValidationException() { - super(); + super(); } public ValidationException(String s) { - super(s); + super(s); } - - private static final long serialVersionUID = 1L; -} +} \ No newline at end of file diff --git a/library/src/main/java/com/google/android/vending/licensing/util/Base64.java b/library/src/main/java/com/google/android/vending/licensing/util/Base64.java index a0d2779..14b1a25 100644 --- a/library/src/main/java/com/google/android/vending/licensing/util/Base64.java +++ b/library/src/main/java/com/google/android/vending/licensing/util/Base64.java @@ -31,87 +31,96 @@ * @version 1.3 */ +import android.annotation.SuppressLint; + /** - * Base64 converter class. This code is not a full-blown MIME encoder; - * it simply converts binary data to base64 data and back. + * Base64 converter class. This code is not a full-blown MIME encoder; it simply converts binary + * data to base64 data and back. * - *

Note {@link CharBase64} is a GWT-compatible implementation of this - * class. + *

Note {@link CharBase64} is a GWT-compatible implementation of this class. */ public class Base64 { - /** Specify encoding (value is {@code true}). */ - public final static boolean ENCODE = true; - - /** Specify decoding (value is {@code false}). */ - public final static boolean DECODE = false; - - /** The equals sign (=) as a byte. */ - private final static byte EQUALS_SIGN = (byte) '='; - - /** The new line character (\n) as a byte. */ - private final static byte NEW_LINE = (byte) '\n'; - - /** - * The 64 valid Base64 values. - */ - private final static byte[] ALPHABET = - {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', - (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', - (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', - (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', - (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', - (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', - (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', - (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', - (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', - (byte) '9', (byte) '+', (byte) '/'}; - - /** - * The 64 valid web safe Base64 values. - */ - private final static byte[] WEBSAFE_ALPHABET = - {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', - (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', - (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', - (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', - (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', - (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', - (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', - (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', - (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', - (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', - (byte) '9', (byte) '-', (byte) '_'}; - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - **/ - private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 + /** + * Specify encoding (value is {@code true}). + */ + public final static boolean ENCODE = true; + + /** + * Specify decoding (value is {@code false}). + */ + public final static boolean DECODE = false; + + /** + * The equals sign (=) as a byte. + */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** + * The new line character (\n) as a byte. + */ + private final static byte NEW_LINE = (byte) '\n'; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET = + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '+', (byte) '/'}; + + /** + * The 64 valid web safe Base64 values. + */ + private final static byte[] WEBSAFE_ALPHABET = + {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '-', (byte) '_'}; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a negative number + * indicating some other meaning. + **/ + private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 @@ -122,32 +131,34 @@ public class Base64 { -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - /** The web safe decodabet */ - private final static byte[] WEBSAFE_DECODABET = - {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 - 62, // Dash '-' sign at decimal 45 - -9, -9, // Decimal 46-47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, // Decimal 91-94 - 63, // Underscore '_' at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9, -9 // Decimal 123 - 127 + }; + + /** + * The web safe decodabet + */ + private final static byte[] WEBSAFE_DECODABET = + {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 + 62, // Dash '-' sign at decimal 45 + -9, -9, // Decimal 46-47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91-94 + 63, // Underscore '_' at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 @@ -158,413 +169,400 @@ public class Base64 { -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; + }; - // Indicates white space in encoding - private final static byte WHITE_SPACE_ENC = -5; - // Indicates equals sign in encoding - private final static byte EQUALS_SIGN_ENC = -1; + // Indicates white space in encoding + private final static byte WHITE_SPACE_ENC = -5; + // Indicates equals sign in encoding + private final static byte EQUALS_SIGN_ENC = -1; - /** Defeats instantiation. */ - private Base64() { - } + /** + * Defeats instantiation. + */ + private Base64() { + } /* ******** E N C O D I N G M E T H O D S ******** */ - /** - * Encodes up to three bytes of the array source - * and writes the resulting four Base64 bytes to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accommodate srcOffset + 3 for - * the source array or destOffset + 4 for - * the destination array. - * The actual number of significant bytes in your array is - * given by numSigBytes. - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param alphabet is the encoding alphabet - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4(byte[] source, int srcOffset, - int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index alphabet - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = - (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) - | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) - | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; - return destination; - case 2: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - case 1: - destination[destOffset] = alphabet[(inBuff >>> 18)]; - destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - default: - return destination; - } // end switch - } // end encode3to4 - - /** - * Encodes a byte array into Base64 notation. - * Equivalent to calling - * {@code encodeBytes(source, 0, source.length)} - * - * @param source The data to convert - * @since 1.4 - */ - public static String encode(byte[] source) { - return encode(source, 0, source.length, ALPHABET, true); - } - - /** - * Encodes a byte array into web safe Base64 notation. - * - * @param source The data to convert - * @param doPadding is {@code true} to pad result with '=' chars - * if it does not fall on 3 byte boundaries - */ - public static String encodeWebSafe(byte[] source, boolean doPadding) { - return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); - } - - /** - * Encodes a byte array into Base64 notation. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param alphabet is the encoding alphabet - * @param doPadding is {@code true} to pad result with '=' chars - * if it does not fall on 3 byte boundaries - * @since 1.4 - */ - public static String encode(byte[] source, int off, int len, byte[] alphabet, - boolean doPadding) { - byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); - int outLen = outBuff.length; - - // If doPadding is false, set length to truncate '=' - // padding characters - while (doPadding == false && outLen > 0) { - if (outBuff[outLen - 1] != '=') { - break; - } - outLen -= 1; + /** + * Encodes up to three bytes of the array source and writes the resulting four Base64 + * bytes to destination. The source and destination arrays can be manipulated + * anywhere along their length by specifying srcOffset and destOffset. + * This method does not check to make sure your arrays are large enough to accommodate + * srcOffset + 3 for the source array or destOffset + 4 for the + * destination array. The actual number of significant bytes in your array is given + * by numSigBytes. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param alphabet is the encoding alphabet + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, + int numSigBytes, byte[] destination, int destOffset, byte[] + alphabet) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index alphabet + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = + (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; + return destination; + case 2: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + case 1: + destination[destOffset] = alphabet[(inBuff >>> 18)]; + destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Encodes a byte array into Base64 notation. Equivalent to calling {@code encodeBytes(source, + * 0, source.length)} + * + * @param source The data to convert + * @since 1.4 + */ + public static String encode(byte[] source) { + return encode(source, 0, source.length, ALPHABET, true); + } + + /** + * Encodes a byte array into web safe Base64 notation. + * + * @param source The data to convert + * @param doPadding is {@code true} to pad result with '=' chars if it does not fall on 3 byte + * boundaries + */ + public static String encodeWebSafe(byte[] source, boolean doPadding) { + return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); } - return new String(outBuff, 0, outLen); - } - - /** - * Encodes a byte array into Base64 notation. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param alphabet is the encoding alphabet - * @param maxLineLength maximum length of one line. - * @return the BASE64-encoded byte array - */ - public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, - int maxLineLength) { - int lenDiv3 = (len + 2) / 3; // ceil(len / 3) - int len43 = lenDiv3 * 4; - byte[] outBuff = new byte[len43 // Main 4:3 - + (len43 / maxLineLength)]; // New lines - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - - // The following block of code is the same as - // encode3to4( source, d + off, 3, outBuff, e, alphabet ); - // but inlined for faster encoding (~20% improvement) - int inBuff = - ((source[d + off] << 24) >>> 8) - | ((source[d + 1 + off] << 24) >>> 16) - | ((source[d + 2 + off] << 24) >>> 24); - outBuff[e] = alphabet[(inBuff >>> 18)]; - outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; - outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; - outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; - - lineLength += 4; - if (lineLength == maxLineLength) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // end for: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, alphabet); - - lineLength += 4; - if (lineLength == maxLineLength) { - // Add a last newline - outBuff[e + 4] = NEW_LINE; - e++; - } - e += 4; + /** + * Encodes a byte array into Base64 notation. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param alphabet is the encoding alphabet + * @param doPadding is {@code true} to pad result with '=' chars if it does not fall on 3 byte + * boundaries + * @since 1.4 + */ + public static String encode(byte[] source, int off, int len, byte[] alphabet, + boolean doPadding) { + byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); + int outLen = outBuff.length; + + // If doPadding is false, set length to truncate '=' + // padding characters + while ((!doPadding) && outLen > 0) { + if (outBuff[outLen - 1] != '=') { + break; + } + outLen -= 1; + } + + return new String(outBuff, 0, outLen); } - assert (e == outBuff.length); - return outBuff; - } + /** + * Encodes a byte array into Base64 notation. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param alphabet is the encoding alphabet + * @param maxLineLength maximum length of one line. + * @return the BASE64-encoded byte array + */ + @SuppressLint("Assert") + public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, + int maxLineLength) { + int lenDiv3 = (len + 2) / 3; // ceil(len / 3) + int len43 = lenDiv3 * 4; + byte[] outBuff = new byte[len43 // Main 4:3 + + (len43 / maxLineLength)]; // New lines + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + + // The following block of code is the same as + // encode3to4( source, d + off, 3, outBuff, e, alphabet ); + // but inlined for faster encoding (~20% improvement) + int inBuff = + ((source[d + off] << 24) >>> 8) + | ((source[d + 1 + off] << 24) >>> 16) + | ((source[d + 2 + off] << 24) >>> 24); + outBuff[e] = alphabet[(inBuff >>> 18)]; + outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; + outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; + outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; + + lineLength += 4; + if (lineLength == maxLineLength) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // end for: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, alphabet); + + lineLength += 4; + if (lineLength == maxLineLength) { + // Add a last newline + outBuff[e + 4] = NEW_LINE; + e++; + } + e += 4; + } + + assert (e == outBuff.length); + return outBuff; + } /* ******** D E C O D I N G M E T H O D S ******** */ - /** - * Decodes four bytes from array source - * and writes the resulting bytes (up to three of them) - * to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accommodate srcOffset + 4 for - * the source array or destOffset + 3 for - * the destination array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - * - * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param decodabet the decodabet for decoding Base64 content - * @return the number of decoded bytes converted - * @since 1.3 - */ - private static int decode4to3(byte[] source, int srcOffset, - byte[] destination, int destOffset, byte[] decodabet) { - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); - - destination[destOffset] = (byte) (outBuff >>> 16); - return 1; - } else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Example: DkL= - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) - | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); - - destination[destOffset] = (byte) (outBuff >>> 16); - destination[destOffset + 1] = (byte) (outBuff >>> 8); - return 2; - } else { - // Example: DkLE - int outBuff = - ((decodabet[source[srcOffset]] << 24) >>> 6) - | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) - | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) - | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); - - destination[destOffset] = (byte) (outBuff >> 16); - destination[destOffset + 1] = (byte) (outBuff >> 8); - destination[destOffset + 2] = (byte) (outBuff); - return 3; + /** + * Decodes four bytes from array source and writes the resulting bytes (up to three + * of them) to destination. The source and destination arrays can be manipulated + * anywhere along their length by specifying srcOffset and destOffset. + * This method does not check to make sure your arrays are large enough to accommodate + * srcOffset + 4 for the source array or destOffset + 3 for the + * destination array. This method returns the actual number of bytes that were + * converted from the Base64 encoding. + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param decodabet the decodabet for decoding Base64 content + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset, byte[] decodabet) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Example: DkL= + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } else { + // Example: DkLE + int outBuff = + ((decodabet[source[srcOffset]] << 24) >>> 6) + | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) + | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) + | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + return 3; + } + } // end decodeToBytes + + + /** + * Decodes data from Base64 notation. + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decode(bytes, 0, bytes.length); + } + + /** + * Decodes data from web safe Base64 notation. Web safe encoding uses '-' instead of '+', '_' + * instead of '/' + * + * @param s the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(String s) throws Base64DecoderException { + byte[] bytes = s.getBytes(); + return decodeWebSafe(bytes, 0, bytes.length); } - } // end decodeToBytes - - - /** - * Decodes data from Base64 notation. - * - * @param s the string to decode (decoded in default encoding) - * @return the decoded data - * @since 1.4 - */ - public static byte[] decode(String s) throws Base64DecoderException { - byte[] bytes = s.getBytes(); - return decode(bytes, 0, bytes.length); - } - - /** - * Decodes data from web safe Base64 notation. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param s the string to decode (decoded in default encoding) - * @return the decoded data - */ - public static byte[] decodeWebSafe(String s) throws Base64DecoderException { - byte[] bytes = s.getBytes(); - return decodeWebSafe(bytes, 0, bytes.length); - } - - /** - * Decodes Base64 content in byte array format and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @return decoded data - * @since 1.3 - * @throws Base64DecoderException - */ - public static byte[] decode(byte[] source) throws Base64DecoderException { - return decode(source, 0, source.length); - } - - /** - * Decodes web safe Base64 content in byte array format and returns - * the decoded data. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param source the string to decode (decoded in default encoding) - * @return the decoded data - */ - public static byte[] decodeWebSafe(byte[] source) - throws Base64DecoderException { - return decodeWebSafe(source, 0, source.length); - } - - /** - * Decodes Base64 content in byte array format and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @return decoded data - * @since 1.3 - * @throws Base64DecoderException - */ - public static byte[] decode(byte[] source, int off, int len) - throws Base64DecoderException { - return decode(source, off, len, DECODABET); - } - - /** - * Decodes web safe Base64 content in byte array format and returns - * the decoded byte array. - * Web safe encoding uses '-' instead of '+', '_' instead of '/' - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @return decoded data - */ - public static byte[] decodeWebSafe(byte[] source, int off, int len) - throws Base64DecoderException { - return decode(source, off, len, WEBSAFE_DECODABET); - } - - /** - * Decodes Base64 content using the supplied decodabet and returns - * the decoded byte array. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @param decodabet the decodabet for decoding Base64 content - * @return decoded data - */ - public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) - throws Base64DecoderException { - int len34 = len * 3 / 4; - byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output - int outBuffPosn = 0; - - byte[] b4 = new byte[4]; - int b4Posn = 0; - int i = 0; - byte sbiCrop = 0; - byte sbiDecode = 0; - for (i = 0; i < len; i++) { - sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits - sbiDecode = decodabet[sbiCrop]; - - if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better - if (sbiDecode >= EQUALS_SIGN_ENC) { - // An equals sign (for padding) must not occur at position 0 or 1 - // and must be the last byte[s] in the encoded value - if (sbiCrop == EQUALS_SIGN) { - int bytesLeft = len - i; - byte lastByte = (byte) (source[len - 1 + off] & 0x7f); - if (b4Posn == 0 || b4Posn == 1) { - throw new Base64DecoderException( - "invalid padding byte '=' at byte offset " + i); - } else if ((b4Posn == 3 && bytesLeft > 2) - || (b4Posn == 4 && bytesLeft > 1)) { - throw new Base64DecoderException( - "padding byte '=' falsely signals end of encoded value " - + "at offset " + i); - } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { - throw new Base64DecoderException( - "encoded value has invalid trailing byte"); + + /** + * Decodes Base64 content in byte array format and returns the decoded byte array. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 1.3 + */ + public static byte[] decode(byte[] source) throws Base64DecoderException { + return decode(source, 0, source.length); + } + + /** + * Decodes web safe Base64 content in byte array format and returns the decoded data. Web safe + * encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source the string to decode (decoded in default encoding) + * @return the decoded data + */ + public static byte[] decodeWebSafe(byte[] source) + throws Base64DecoderException { + return decodeWebSafe(source, 0, source.length); + } + + /** + * Decodes Base64 content in byte array format and returns the decoded byte array. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + * @since 1.3 + */ + public static byte[] decode(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, DECODABET); + } + + /** + * Decodes web safe Base64 content in byte array format and returns the decoded byte array. Web + * safe encoding uses '-' instead of '+', '_' instead of '/' + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @return decoded data + */ + public static byte[] decodeWebSafe(byte[] source, int off, int len) + throws Base64DecoderException { + return decode(source, off, len, WEBSAFE_DECODABET); + } + + /** + * Decodes Base64 content using the supplied decodabet and returns the decoded byte array. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param decodabet the decodabet for decoding Base64 content + * @return decoded data + */ + @SuppressWarnings("UnusedAssignment") + public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) + throws Base64DecoderException { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i; + byte sbiCrop; + byte sbiDecode; + for (i = 0; i < len; i++) { + sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits + sbiDecode = decodabet[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better + if (sbiDecode >= EQUALS_SIGN_ENC) { + // An equals sign (for padding) must not occur at position 0 or 1 + // and must be the last byte[s] in the encoded value + if (sbiCrop == EQUALS_SIGN) { + int bytesLeft = len - i; + byte lastByte = (byte) (source[len - 1 + off] & 0x7f); + if (b4Posn == 0 || b4Posn == 1) { + throw new Base64DecoderException( + "invalid padding byte '=' at byte offset " + i); + } else if ((b4Posn == 3 && bytesLeft > 2) + || (b4Posn == 4 && bytesLeft > 1)) { + throw new Base64DecoderException( + "padding byte '=' falsely signals end of encoded value " + + "at offset " + i); + } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { + throw new Base64DecoderException( + "encoded value has invalid trailing byte"); + } + break; + } + + b4[b4Posn++] = sbiCrop; + if (b4Posn == 4) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + b4Posn = 0; + } + } + } else { + throw new Base64DecoderException("Bad Base64 input character at " + i + + ": " + source[i + off] + "(decimal)"); } - break; - } + } - b4[b4Posn++] = sbiCrop; - if (b4Posn == 4) { + // Because web safe encoding allows non padding base64 encodes, we + // need to pad the rest of the b4 buffer with equal signs when + // b4Posn != 0. There can be at most 2 equal signs at the end of + // four characters, so the b4 buffer must have two or three + // characters. This also catches the case where the input is + // padded with EQUALS_SIGN + if (b4Posn != 0) { + if (b4Posn == 1) { + throw new Base64DecoderException("single trailing character at offset " + + (len - 1)); + } + b4[b4Posn++] = EQUALS_SIGN; outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); - b4Posn = 0; - } } - } else { - throw new Base64DecoderException("Bad Base64 input character at " + i - + ": " + source[i + off] + "(decimal)"); - } - } - // Because web safe encoding allows non padding base64 encodes, we - // need to pad the rest of the b4 buffer with equal signs when - // b4Posn != 0. There can be at most 2 equal signs at the end of - // four characters, so the b4 buffer must have two or three - // characters. This also catches the case where the input is - // padded with EQUALS_SIGN - if (b4Posn != 0) { - if (b4Posn == 1) { - throw new Base64DecoderException("single trailing character at offset " - + (len - 1)); - } - b4[b4Posn++] = EQUALS_SIGN; - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; } - - byte[] out = new byte[outBuffPosn]; - System.arraycopy(outBuff, 0, out, 0, outBuffPosn); - return out; - } } diff --git a/library/src/main/java/com/google/android/vending/licensing/util/Base64DecoderException.java b/library/src/main/java/com/google/android/vending/licensing/util/Base64DecoderException.java index 1aef1b5..d4d75cd 100644 --- a/library/src/main/java/com/google/android/vending/licensing/util/Base64DecoderException.java +++ b/library/src/main/java/com/google/android/vending/licensing/util/Base64DecoderException.java @@ -20,13 +20,13 @@ * @author nelson */ public class Base64DecoderException extends Exception { - public Base64DecoderException() { - super(); - } + private static final long serialVersionUID = 1L; - public Base64DecoderException(String s) { - super(s); - } + public Base64DecoderException() { + super(); + } - private static final long serialVersionUID = 1L; + public Base64DecoderException(String s) { + super(s); + } } diff --git a/library/src/main/java/com/google/android/vending/licensing/util/URIQueryDecoder.java b/library/src/main/java/com/google/android/vending/licensing/util/URIQueryDecoder.java new file mode 100644 index 0000000..f34eedc --- /dev/null +++ b/library/src/main/java/com/google/android/vending/licensing/util/URIQueryDecoder.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.vending.licensing.util; + +import android.util.Log; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Map; +import java.util.Scanner; + +public class URIQueryDecoder { + private static final String TAG = "URIQueryDecoder"; + + /** + * Decodes the query portion of the passed-in URI. + * + * @param encodedURI the URI containing the query to decode + * @param results a map containing all query parameters. Query parameters that do not have a + * value will map to a null string + */ + static public void DecodeQuery(URI encodedURI, Map results) { + Scanner scanner = new Scanner(encodedURI.getRawQuery()); + scanner.useDelimiter("&"); + try { + while (scanner.hasNext()) { + String param = scanner.next(); + String[] valuePair = param.split("="); + String name, value; + if (valuePair.length == 1) { + value = null; + } else if (valuePair.length == 2) { + value = URLDecoder.decode(valuePair[1], "UTF-8"); + } else { + throw new IllegalArgumentException("query parameter invalid"); + } + name = URLDecoder.decode(valuePair[0], "UTF-8"); + results.put(name, value); + } + } catch (UnsupportedEncodingException e) { + // This should never happen. + Log.e(TAG, "UTF-8 Not Recognized as a charset. Device configuration Error."); + } + } +} \ No newline at end of file From 6add3ec398898e7ba2e904a9c580ef3dc1c24920 Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 14:04:08 -0500 Subject: [PATCH 04/20] Update proguard-rules.pro --- library/proguard-rules.pro | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro index 17187e5..dc93c57 100644 --- a/library/proguard-rules.pro +++ b/library/proguard-rules.pro @@ -17,10 +17,9 @@ #} # Apache and Google --dontwarn org.apache.** -keep class com.google.** --dontwarn com.google.** -keep class autovalue.shaded.com.google.** +-dontwarn com.google.** -dontwarn autovalue.shaded.com.google.** # LVL From 96b66993c4b4f71988001fb40f55b241c406fecb Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 14:15:13 -0500 Subject: [PATCH 05/20] Add Spanish values. --- library/src/main/res/values-es/piracychecker_strings.xml | 4 ++++ library/src/main/res/values-es/strings.xml | 5 +++++ library/src/main/res/values-sk/strings.xml | 2 +- library/src/main/res/values/piracychecker_strings.xml | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 library/src/main/res/values-es/piracychecker_strings.xml create mode 100644 library/src/main/res/values-es/strings.xml diff --git a/library/src/main/res/values-es/piracychecker_strings.xml b/library/src/main/res/values-es/piracychecker_strings.xml new file mode 100644 index 0000000..d1e3a56 --- /dev/null +++ b/library/src/main/res/values-es/piracychecker_strings.xml @@ -0,0 +1,4 @@ + + + PiracyChecker previene que tu app sea pirateada / crackeada usando Google Play Licensing (LVL), protección de firma de APK y más. + \ No newline at end of file diff --git a/library/src/main/res/values-es/strings.xml b/library/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..0d3b3d0 --- /dev/null +++ b/library/src/main/res/values-es/strings.xml @@ -0,0 +1,5 @@ + + Ésta app no tiene licencia válida + Ésta app no tiene licencia ni es válida. Por favor descargala de una fuente segura. + Cerrar + \ No newline at end of file diff --git a/library/src/main/res/values-sk/strings.xml b/library/src/main/res/values-sk/strings.xml index 9d062e9..e045837 100644 --- a/library/src/main/res/values-sk/strings.xml +++ b/library/src/main/res/values-sk/strings.xml @@ -2,4 +2,4 @@ Táto aplikácia nie je licencovaná Táto aplikácia nie je licencovaná alebo platná. Prosím, stiahnite aplikáciu z dôveryhodného zdroja. Zavrieť - + \ No newline at end of file diff --git a/library/src/main/res/values/piracychecker_strings.xml b/library/src/main/res/values/piracychecker_strings.xml index c5717fb..c57910d 100644 --- a/library/src/main/res/values/piracychecker_strings.xml +++ b/library/src/main/res/values/piracychecker_strings.xml @@ -9,4 +9,4 @@ apache_2_0 true https://github.com/javiersantos/PiracyChecker - + \ No newline at end of file From 6d2dfa70e763531402c6173e57f88930bec0009b Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 14:16:02 -0500 Subject: [PATCH 06/20] Update .travis.yml --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2367920..9c5a9a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: android -jdk: oraclejdk7 +jdk: oraclejdk8 android: components: - - build-tools-23.0.2 - - android-23 + - build-tools-25.0.2 + - android-25 - extra-android-support - extra-google-m2repository - extra-android-m2repository @@ -13,8 +13,8 @@ android: before_script: - echo yes | android update sdk --no-ui --all --filter platform-tools,tools - - echo yes | android update sdk --no-ui --all --filter build-tools-23.0.2 - - echo yes | android update sdk --no-ui --all --filter android-23 + - echo yes | android update sdk --no-ui --all --filter build-tools-25.0.2 + - echo yes | android update sdk --no-ui --all --filter android-25 script: - - ./gradlew clean test + - ./gradlew clean test \ No newline at end of file From 898db87f05f337db0ff436ce1a8291de441ba7e5 Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 14:25:50 -0500 Subject: [PATCH 07/20] Update gradle --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f551c5f..df99522 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' + classpath 'com.android.tools.build:gradle:2.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 122a0dc..b600826 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip \ No newline at end of file From c482ae882213f8b471988ad9f675ed931c1e840a Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 14:29:12 -0500 Subject: [PATCH 08/20] Fix typos in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dfab9c1..d16af3e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

An Android library that prevents your app from being pirated / cracked using Google Play Licensing (LVL), APK signature protection and more.

## Disclaimer -This library applies some techniques to help protect your app's users and attempt to thwart reverse engineers and attackers. BUT, this isn't guaranteed stop your app from getting pirated. There is no such thing as 100% security, and a determined and skilled attacker with enougth time could remove these checks from the code. The real objective here is to raise the bar out of reach of opportunist and automatic attackers. +This library applies some techniques to help protect your app's users and attempt to thwart reverse engineers and attackers. BUT, this isn't guaranteed to stop your app from getting pirated. There is no such thing as 100% security, and a determined and skilled attacker with enough time, could remove these checks from the code. The real objective here is to raise the bar out of reach of opportunist and automatic attackers. Some of the techniques included in this library can be found [here](https://www.airpair.com/android/posts/adding-tampering-detection-to-your-android-app). From 61c1c6e3e5a49696fb5d14332fceadd6b5badadb Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 14:31:10 -0500 Subject: [PATCH 09/20] Attempt to fix travis builds --- .travis.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9c5a9a1..99e64a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,18 +3,12 @@ jdk: oraclejdk8 android: components: - - build-tools-25.0.2 + - tools + - platform-tools + - build-tools-25.0.1 - android-25 - extra-android-support - - extra-google-m2repository - extra-android-m2repository + - extra-google-m2repository licenses: - - '.+' - -before_script: - - echo yes | android update sdk --no-ui --all --filter platform-tools,tools - - echo yes | android update sdk --no-ui --all --filter build-tools-25.0.2 - - echo yes | android update sdk --no-ui --all --filter android-25 - -script: - - ./gradlew clean test \ No newline at end of file + - '.+' \ No newline at end of file From dec9a1da0e83fe43d041041da67df7d91da075d5 Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 14:39:09 -0500 Subject: [PATCH 10/20] Update .travis.yml --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 99e64a0..3ed6037 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,12 @@ android: - extra-android-m2repository - extra-google-m2repository licenses: - - '.+' \ No newline at end of file + - '.+' + +before_script: + - echo yes | android update sdk --no-ui --all --filter platform-tools,tools + - echo yes | android update sdk --no-ui --all --filter build-tools-25.0.2 + - echo yes | android update sdk --no-ui --all --filter android-25 + +script: + - ./gradlew clean test \ No newline at end of file From f2709cecb1458d3c2053c8ec9e77a9587296a491 Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Wed, 18 Jan 2017 14:42:30 -0500 Subject: [PATCH 11/20] Add missing permission from sample app. --- app/src/main/AndroidManifest.xml | 33 +++++++++++-------- .../piracychecker/demo/AboutActivity.java | 2 +- .../piracychecker/demo/MainActivity.java | 13 ++++---- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 63ef0d9..76cd8df 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,28 +1,33 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.github.javiersantos.piracychecker.demo"> + + + android:allowBackup="true" + android:fullBackupContent="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> + android:name=".MainActivity" + android:label="@string/app_name" + android:theme="@style/AppTheme.NoActionBar"> - + - + - + android:name=".AboutActivity" + android:label="@string/action_about"/> + \ No newline at end of file diff --git a/app/src/main/java/com/github/javiersantos/piracychecker/demo/AboutActivity.java b/app/src/main/java/com/github/javiersantos/piracychecker/demo/AboutActivity.java index 2ccea72..8313ff7 100644 --- a/app/src/main/java/com/github/javiersantos/piracychecker/demo/AboutActivity.java +++ b/app/src/main/java/com/github/javiersantos/piracychecker/demo/AboutActivity.java @@ -24,4 +24,4 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/javiersantos/piracychecker/demo/MainActivity.java b/app/src/main/java/com/github/javiersantos/piracychecker/demo/MainActivity.java index 27bcb36..7eb2c1a 100644 --- a/app/src/main/java/com/github/javiersantos/piracychecker/demo/MainActivity.java +++ b/app/src/main/java/com/github/javiersantos/piracychecker/demo/MainActivity.java @@ -32,11 +32,13 @@ protected void onCreate(Bundle savedInstanceState) { FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); - fab.setImageDrawable(new IconicsDrawable(this).icon(MaterialDesignIconic.Icon.gmi_github).color(Color.WHITE).sizeDp(24)); + fab.setImageDrawable(new IconicsDrawable(this).icon(MaterialDesignIconic.Icon.gmi_github) + .color(Color.WHITE).sizeDp(24)); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/javiersantos/PiracyChecker"))); + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github" + + ".com/javiersantos/PiracyChecker"))); } }); } @@ -65,9 +67,8 @@ public void verifyInstallerId(View view) { public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_main, menu); - - menu.findItem(R.id.action_about).setIcon(new IconicsDrawable(this).icon(MaterialDesignIconic.Icon.gmi_info).color(Color.WHITE).actionBar()); - + menu.findItem(R.id.action_about).setIcon(new IconicsDrawable(this).icon + (MaterialDesignIconic.Icon.gmi_info).color(Color.WHITE).actionBar()); return true; } @@ -81,4 +82,4 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } } -} +} \ No newline at end of file From 2b321a30e37ca19081b7045c40405eeedcb8b5f1 Mon Sep 17 00:00:00 2001 From: Javier Santos Date: Thu, 19 Jan 2017 00:09:52 +0100 Subject: [PATCH 12/20] Update .travis.yml --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3ed6037..ac2cde7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,11 @@ jdk: oraclejdk8 android: components: - - tools - - platform-tools - - build-tools-25.0.1 + - build-tools-25.0.2 - android-25 - extra-android-support - - extra-android-m2repository - extra-google-m2repository + - extra-android-m2repository licenses: - '.+' @@ -19,4 +17,4 @@ before_script: - echo yes | android update sdk --no-ui --all --filter android-25 script: - - ./gradlew clean test \ No newline at end of file + - ./gradlew clean test From 6abd3184c99e463894cb977ddc7c0c2cbd43ecb8 Mon Sep 17 00:00:00 2001 From: Javier Santos Date: Thu, 19 Jan 2017 00:12:24 +0100 Subject: [PATCH 13/20] Update .gitignore --- .gitignore | 83 +++++++++++++++--------------------------------------- 1 file changed, 23 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index 3d80727..f6b286c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,77 +1,40 @@ -############################################################################# -# -# .gitignore declares what files should be ignored by git -# -# This particular file is a composite of github's Android and Eclipse -# files, along with some custom additions. -# -############################################################################# - -############################################################################# -# Eclipse related files -############################################################################# - -*.pydevproject -.metadata -.gradle -bin/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# CDT-specific -.cproject - -# PDT-specific -.buildpath - -# sbteclipse plugin -.target - -# TeXlipse plugin -.texlipse - -############################################################################# -# Android related files -############################################################################# - # Built application files *.apk -#*.ap_ +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class # Generated files +bin/ gen/ +out/ # Gradle files +.gradle/ build/ +# Local configuration file (sdk path, etc) +local.properties + # Proguard folder generated by Eclipse proguard/ -#Log Files +# Log Files *.log -############################################################################# -# Other Misc. files -############################################################################# +# Android Studio Navigation editor temp files +.navigation/ -# Temp files for KDE and other Editor's -*~ +# Android Studio captures folder +captures/ -############################################################################# -# Android Studio related files -############################################################################# +# Intellij +*.iml +.idea/workspace.xml -.idea/ -#gradle.properties -*.iml \ No newline at end of file +# Keystore files +*.jks From 8c655504bb6e6670b22d1886e59f135c92c054dd Mon Sep 17 00:00:00 2001 From: Javier Santos Date: Thu, 19 Jan 2017 00:13:49 +0100 Subject: [PATCH 14/20] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d16af3e..90703e6 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ dependencies { ``` ## Usage +Add **INTERNET** and **ACCESS_NETWORK_STATE** permissions to your app's Manifest: +```xml + + +``` + ### Verify Google Play Licensing (LVL) Google Play offers a licensing service that lets you enforce licensing policies for applications that you publish on Google Play. With Google Play Licensing, your application can query Google Play to obtain the licensing status for the current user. From 8a3156ff07216644ac44be1ae98fcf16ae2d3a58 Mon Sep 17 00:00:00 2001 From: Javier Santos Date: Thu, 19 Jan 2017 00:15:46 +0100 Subject: [PATCH 15/20] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 90703e6..c1594ac 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,9 @@ dependencies { ``` ## Usage -Add **INTERNET** and **ACCESS_NETWORK_STATE** permissions to your app's Manifest: +Add **CHECK_LICENSE** permission to your app's Manifest: ```xml - - + ``` ### Verify Google Play Licensing (LVL) From 62bdc880bab0700f7326a59ca42e33306c2682cb Mon Sep 17 00:00:00 2001 From: Javier Santos Date: Thu, 19 Jan 2017 00:22:18 +0100 Subject: [PATCH 16/20] Update .gitignore --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f6b286c..fdf83fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # Built application files -*.apk *.ap_ # Files for the ART/Dalvik VM @@ -32,9 +31,11 @@ proguard/ # Android Studio captures folder captures/ +# Directory-based project format +.idea/ + # Intellij *.iml -.idea/workspace.xml # Keystore files *.jks From fba8a12180507cb5ba58bad7a992943308e7cb00 Mon Sep 17 00:00:00 2001 From: Javier Santos Date: Thu, 19 Jan 2017 00:30:25 +0100 Subject: [PATCH 17/20] Update MainActivity.java --- .../github/javiersantos/piracychecker/demo/MainActivity.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/github/javiersantos/piracychecker/demo/MainActivity.java b/app/src/main/java/com/github/javiersantos/piracychecker/demo/MainActivity.java index 7eb2c1a..0afea0e 100644 --- a/app/src/main/java/com/github/javiersantos/piracychecker/demo/MainActivity.java +++ b/app/src/main/java/com/github/javiersantos/piracychecker/demo/MainActivity.java @@ -37,8 +37,7 @@ protected void onCreate(Bundle savedInstanceState) { fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github" + - ".com/javiersantos/PiracyChecker"))); + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/javiersantos/PiracyChecker"))); } }); } @@ -82,4 +81,4 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } } -} \ No newline at end of file +} From df85baf38418dbe0fd549218b984d4c19fe05fd5 Mon Sep 17 00:00:00 2001 From: Javier Santos Date: Thu, 19 Jan 2017 00:55:17 +0100 Subject: [PATCH 18/20] Update strings.xml --- library/src/main/res/values-es/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/src/main/res/values-es/strings.xml b/library/src/main/res/values-es/strings.xml index 0d3b3d0..f63c5c8 100644 --- a/library/src/main/res/values-es/strings.xml +++ b/library/src/main/res/values-es/strings.xml @@ -1,5 +1,5 @@ - Ésta app no tiene licencia válida - Ésta app no tiene licencia ni es válida. Por favor descargala de una fuente segura. + Licencia no válida + Ésta app no dispone de una licencia válida. Por favor, descárgala de una fuente segura. Cerrar - \ No newline at end of file + From 526292963bd2c632b7b9b3d829257878fa67a8d3 Mon Sep 17 00:00:00 2001 From: Javier Santos Date: Thu, 19 Jan 2017 11:26:41 +0100 Subject: [PATCH 19/20] Update .travis.yml --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ac2cde7..25b1032 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,10 @@ android: - extra-android-m2repository licenses: - '.+' - + +before_install: + - chmod +x gradlew + before_script: - echo yes | android update sdk --no-ui --all --filter platform-tools,tools - echo yes | android update sdk --no-ui --all --filter build-tools-25.0.2 From 6066d2fc6f32fad1054b6b6aebff28ea4f555682 Mon Sep 17 00:00:00 2001 From: Jahir Fiquitiva Date: Tue, 24 Jan 2017 19:47:57 -0500 Subject: [PATCH 20/20] Allow users to manage the license check errors by themselves. --- README.md | 14 ++++++++--- .../{UtilsLibrary.java => LibraryUtils.java} | 6 +++-- .../piracychecker/PiracyChecker.java | 22 +++++++++------- .../piracychecker/PiracyCheckerUtils.java | 25 +++++++++++++++++-- .../enums/PiracyCheckerCallback.java | 23 +++++++++++++---- .../enums/PiracyCheckerError.java | 11 +++++++- .../licensing/LicenseCheckerCallback.java | 3 ++- 7 files changed, 80 insertions(+), 24 deletions(-) rename library/src/main/java/com/github/javiersantos/piracychecker/{UtilsLibrary.java => LibraryUtils.java} (95%) diff --git a/README.md b/README.md index c1594ac..b35d8f5 100644 --- a/README.md +++ b/README.md @@ -99,14 +99,20 @@ Use the builder and add following: @Override public void allow() { // Do something when the user is allowed to use the app - } @Override - public void dontAllow() { - // Do something when the user is not allowed to use the app - + public void dontAllow(PiracyCheckerError error) { + // You can either do something specific when the user is not allowed to use the app + // Or manage the error, using the 'error' parameter, yourself (Check errors at {@link PiracyCheckerError}). } + + @Override + public void onError(PiracyCheckerError error) { + // This method is not required to be implemented/overriden but... + // You can either do something specific when an error occurs while checking the license, + // Or manage the error, using the 'error' parameter, yourself (Check errors at {@link PiracyCheckerError}). + } ``` ## ProGuard diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/UtilsLibrary.java b/library/src/main/java/com/github/javiersantos/piracychecker/LibraryUtils.java similarity index 95% rename from library/src/main/java/com/github/javiersantos/piracychecker/UtilsLibrary.java rename to library/src/main/java/com/github/javiersantos/piracychecker/LibraryUtils.java index fa9b7a5..46b6cda 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/UtilsLibrary.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/LibraryUtils.java @@ -10,6 +10,7 @@ import android.support.v7.app.AlertDialog; import android.util.Base64; +import com.github.javiersantos.piracychecker.R; import com.github.javiersantos.piracychecker.enums.InstallerID; import java.security.MessageDigest; @@ -18,7 +19,7 @@ import java.util.List; @SuppressLint("PackageManagerGetSignatures") -class UtilsLibrary { +class LibraryUtils { static final byte[] SALT = new byte[]{ -46, 65, 30, -128, -103, -57, 74, -64, 51, 88, -95, -45, 77, -117, -36, -113, -11, 32, -64, 89 @@ -38,7 +39,8 @@ public void onClick(DialogInterface dialogInterface, int i) { return; activity.finish(); } - }).create(); + }) + .create(); } static String getCurrentSignature(Context context) { diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/PiracyChecker.java b/library/src/main/java/com/github/javiersantos/piracychecker/PiracyChecker.java index 1c361c1..c3f0ac6 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/PiracyChecker.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/PiracyChecker.java @@ -73,20 +73,22 @@ public PiracyChecker callback(PiracyCheckerCallback callback) { } public void start() { - if (callback != null) + if (callback != null) { verify(callback); - else - verify(new PiracyCheckerCallback() { + } else { + this.callback = new PiracyCheckerCallback() { @Override public void allow() { } @Override public void dontAllow(PiracyCheckerError error) { - UtilsLibrary.buildUnlicensedDialog(context, unlicensedDialogTitle, + LibraryUtils.buildUnlicensedDialog(context, unlicensedDialogTitle, unlicensedDialogDescription).show(); } - }); + }; + verify(callback); + } } protected void verify(final PiracyCheckerCallback verifyCallback) { @@ -101,7 +103,7 @@ protected void verify(final PiracyCheckerCallback verifyCallback) { String deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); LicenseChecker licenseChecker = new LicenseChecker(context, new - ServerManagedPolicy(context, new AESObfuscator(UtilsLibrary.SALT, context + ServerManagedPolicy(context, new AESObfuscator(LibraryUtils.SALT, context .getPackageName(), deviceId)), licenseBase64); licenseChecker.checkAccess(new LicenseCheckerCallback() { @Override @@ -116,6 +118,8 @@ public void dontAllow(int reason) { @Override public void applicationError(int errorCode) { + verifyCallback.onError(PiracyCheckerUtils.getCheckerErrorFromCode + (errorCode)); } }); } else { @@ -126,7 +130,7 @@ public void applicationError(int errorCode) { protected boolean verifySigningCertificate() { if (enableSigningCertificate) { - if (UtilsLibrary.verifySigningCertificate(context, signature)) { + if (LibraryUtils.verifySigningCertificate(context, signature)) { return true; } } else { @@ -137,7 +141,7 @@ protected boolean verifySigningCertificate() { protected boolean verifyInstallerId() { if (enableInstallerId) { - if (UtilsLibrary.verifyInstallerId(context, installerIDs)) { + if (LibraryUtils.verifyInstallerId(context, installerIDs)) { return true; } } else { @@ -146,4 +150,4 @@ protected boolean verifyInstallerId() { return false; } -} +} \ No newline at end of file diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/PiracyCheckerUtils.java b/library/src/main/java/com/github/javiersantos/piracychecker/PiracyCheckerUtils.java index 65f8510..475494d 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/PiracyCheckerUtils.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/PiracyCheckerUtils.java @@ -2,10 +2,31 @@ import android.content.Context; +import com.github.javiersantos.piracychecker.enums.PiracyCheckerError; + public class PiracyCheckerUtils { public static String getAPKSignature(Context context) { - return UtilsLibrary.getCurrentSignature(context); + return LibraryUtils.getCurrentSignature(context); + } + + public static PiracyCheckerError getCheckerErrorFromCode(int errorCode) { + switch (errorCode) { + case 1: + return PiracyCheckerError.INVALID_PACKAGE_NAME; + case 2: + return PiracyCheckerError.NON_MATCHING_UID; + case 3: + return PiracyCheckerError.NOT_MARKET_MANAGED; + case 4: + return PiracyCheckerError.CHECK_IN_PROGRESS; + case 5: + return PiracyCheckerError.INVALID_PUBLIC_KEY; + case 6: + return PiracyCheckerError.MISSING_PERMISSION; + default: + return PiracyCheckerError.UNKNOWN; + } } -} +} \ No newline at end of file diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerCallback.java b/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerCallback.java index 00f1d6f..933df8f 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerCallback.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerCallback.java @@ -1,18 +1,31 @@ package com.github.javiersantos.piracychecker.enums; -public interface PiracyCheckerCallback { +public abstract class PiracyCheckerCallback { /** * Called after the app checked as valid and licensed */ - void allow(); + public abstract void allow(); /** - * Called if the app is not valid or the user is using an unlicensed version + * Called if the app is not valid or the user is using an unlicensed version. Check errors at + * {@link PiracyCheckerError}. * * @param error PiracyCheckerError.NOT_LICENSED, PiracyCheckerError.SIGNATURE_NOT_VALID or * PiracyCheckerError.INVALID_INSTALLER_ID */ - void dontAllow(PiracyCheckerError error); + public abstract void dontAllow(PiracyCheckerError error); -} + /** + * Called if an error with the license check occurs. Check errors at {@link + * PiracyCheckerError}. + * + * @param error PiracyCheckerError.INVALID_PACKAGE_NAME, PiracyCheckerError.NON_MATCHING_UID, + * PiracyCheckerError.NOT_MARKET_MANAGED, PiracyCheckerError.CHECK_IN_PROGRESS, + * PiracyCheckerError.INVALID_PUBLIC_KEY, PiracyCheckerError.MISSING_PERMISSION or + * PiracyCheckerError.UNKNOWN + */ + public void onError(PiracyCheckerError error) { + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerError.java b/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerError.java index ab15478..258981b 100644 --- a/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerError.java +++ b/library/src/main/java/com/github/javiersantos/piracychecker/enums/PiracyCheckerError.java @@ -3,7 +3,16 @@ public enum PiracyCheckerError { NOT_LICENSED("This user is not using a licensed application from Google Play."), SIGNATURE_NOT_VALID("This app is using another signature. The original APK has been modified."), - INVALID_INSTALLER_ID("This app has been installed from a non-allowed source."); + INVALID_INSTALLER_ID("This app has been installed from a non-allowed source."), + // Other errors + INVALID_PACKAGE_NAME("Application package name is invalid."), + NON_MATCHING_UID("Application UID doesn't match."), + NOT_MARKET_MANAGED("Not market managed error."), + CHECK_IN_PROGRESS("License check is in progress."), + INVALID_PUBLIC_KEY("Application public key is invalid."), + MISSING_PERMISSION("Application misses the 'com.android.vending.CHECK_LICENSE' " + + "permission."), + UNKNOWN("Unknown error."); private final String text; diff --git a/library/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java b/library/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java index 08d2b61..bc43b93 100644 --- a/library/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java +++ b/library/src/main/java/com/google/android/vending/licensing/LicenseCheckerCallback.java @@ -60,4 +60,5 @@ public interface LicenseCheckerCallback { * considered fatal. */ void applicationError(int errorCode); -} + +} \ No newline at end of file