From 32db6818cf798d9b96df050652c2882c99a6874c Mon Sep 17 00:00:00 2001 From: amirisback Date: Wed, 21 Jun 2023 17:08:54 +0700 Subject: [PATCH 1/5] add: update piracy checker --- app/src/main/AndroidManifest.xml | 11 +- .../frogobox/appsdk/piracy/KotlinActivity.kt | 124 ++++ .../appsdk/piracy/PiracyMainActivity.java | 109 +++ app/src/main/res/layout/activity_piracy.xml | 90 +++ app/src/main/res/layout/content_piracy.xml | 234 +++++++ app/src/main/res/values/strings.xml | 19 +- app/src/main/res/values/styles.xml | 9 + buildSrc/src/main/kotlin/DependencyGradle.kt | 1 + buildSrc/src/main/kotlin/ProjectSetting.kt | 1 + core-sdk-android-util/.gitignore | 1 + core-sdk-android-util/build.gradle.kts | 74 ++ core-sdk-android-util/proguard-rules.pro | 32 + .../src/main/AndroidManifest.xml | 12 + .../licensing/ILicenseResultListener.aidl | 21 + .../vending/licensing/ILicensingService.aidl | 23 + .../sdkutil/licensing/AESObfuscator.java | 106 +++ .../sdkutil/licensing/APKExpansionPolicy.java | 375 +++++++++++ .../sdkutil/licensing/DeviceLimiter.java | 43 ++ .../sdkutil/licensing/LibraryChecker.java | 396 +++++++++++ .../licensing/LibraryCheckerCallback.java | 66 ++ .../sdkutil/licensing/LibraryValidator.java | 232 +++++++ .../sdkutil/licensing/NullDeviceLimiter.java | 30 + .../sdkutil/licensing/Obfuscator.java | 55 ++ .../frogobox/sdkutil/licensing/Policy.java | 60 ++ .../licensing/PreferenceObfuscator.java | 81 +++ .../sdkutil/licensing/ResponseData.java | 87 +++ .../licensing/ServerManagedPolicy.java | 264 ++++++++ .../sdkutil/licensing/StrictPolicy.java | 59 ++ .../licensing/ValidationException.java | 33 + .../sdkutil/licensing/util/Base64.java | 630 ++++++++++++++++++ .../util/Base64DecoderException.java | 32 + .../licensing/util/URIQueryDecoder.java | 62 ++ .../sdkutil/piracychecker/Extensions.kt | 48 ++ .../sdkutil/piracychecker/PiracyChecker.kt | 485 ++++++++++++++ .../piracychecker/PiracyCheckerDialog.kt | 36 + .../piracychecker/activities/ActivityUtils.kt | 36 + .../activities/LicenseActivity.kt | 78 +++ .../callbacks/PiracyCheckerCallbacks.kt | 47 ++ .../callbacks/PiracyCheckerCallbacksDSL.kt | 14 + .../sdkutil/piracychecker/enums/AppType.kt | 3 + .../sdkutil/piracychecker/enums/Display.kt | 3 + .../piracychecker/enums/InstallerID.kt | 24 + .../piracychecker/enums/PiracyCheckerError.kt | 41 ++ .../sdkutil/piracychecker/enums/PirateApp.kt | 32 + .../piracychecker/utils/LibraryUtils.kt | 628 +++++++++++++++++ .../sdkutil/piracychecker/utils/SaltUtils.kt | 65 ++ .../src/main/res/layout/activity_license.xml | 30 + .../res/layout/activity_license_default.xml | 30 + .../res/values-es/piracychecker_strings.xml | 4 + .../src/main/res/values-es/strings.xml | 6 + .../src/main/res/values-sk/strings.xml | 6 + .../src/main/res/values/colors.xml | 6 + .../main/res/values/piracychecker_strings.xml | 12 + .../src/main/res/values/strings.xml | 8 + .../src/main/res/values/styles.xml | 17 + core-sdk-android/build.gradle.kts | 18 +- .../delegate/piracy/PiracyDelegatesImpl.kt | 12 +- gradle.properties | 2 +- settings.gradle.kts | 3 +- 59 files changed, 5038 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/frogobox/appsdk/piracy/KotlinActivity.kt create mode 100644 app/src/main/java/com/frogobox/appsdk/piracy/PiracyMainActivity.java create mode 100644 app/src/main/res/layout/activity_piracy.xml create mode 100644 app/src/main/res/layout/content_piracy.xml create mode 100644 core-sdk-android-util/.gitignore create mode 100644 core-sdk-android-util/build.gradle.kts create mode 100644 core-sdk-android-util/proguard-rules.pro create mode 100644 core-sdk-android-util/src/main/AndroidManifest.xml create mode 100644 core-sdk-android-util/src/main/aidl/com/android/vending/licensing/ILicenseResultListener.aidl create mode 100644 core-sdk-android-util/src/main/aidl/com/android/vending/licensing/ILicensingService.aidl create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/AESObfuscator.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/APKExpansionPolicy.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/DeviceLimiter.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryChecker.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryCheckerCallback.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryValidator.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/NullDeviceLimiter.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/Obfuscator.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/Policy.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/PreferenceObfuscator.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ResponseData.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ServerManagedPolicy.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/StrictPolicy.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ValidationException.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/Base64.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/Base64DecoderException.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/URIQueryDecoder.java create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/Extensions.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/PiracyChecker.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/PiracyCheckerDialog.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/activities/ActivityUtils.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/activities/LicenseActivity.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/callbacks/PiracyCheckerCallbacks.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/callbacks/PiracyCheckerCallbacksDSL.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/AppType.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/Display.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/InstallerID.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/PiracyCheckerError.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/PirateApp.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/utils/LibraryUtils.kt create mode 100644 core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/utils/SaltUtils.kt create mode 100644 core-sdk-android-util/src/main/res/layout/activity_license.xml create mode 100644 core-sdk-android-util/src/main/res/layout/activity_license_default.xml create mode 100644 core-sdk-android-util/src/main/res/values-es/piracychecker_strings.xml create mode 100644 core-sdk-android-util/src/main/res/values-es/strings.xml create mode 100644 core-sdk-android-util/src/main/res/values-sk/strings.xml create mode 100644 core-sdk-android-util/src/main/res/values/colors.xml create mode 100644 core-sdk-android-util/src/main/res/values/piracychecker_strings.xml create mode 100644 core-sdk-android-util/src/main/res/values/strings.xml create mode 100644 core-sdk-android-util/src/main/res/values/styles.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f1ec19a..d939c72 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ - + @@ -46,6 +46,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appsdk/piracy/KotlinActivity.kt b/app/src/main/java/com/frogobox/appsdk/piracy/KotlinActivity.kt new file mode 100644 index 0000000..6394098 --- /dev/null +++ b/app/src/main/java/com/frogobox/appsdk/piracy/KotlinActivity.kt @@ -0,0 +1,124 @@ +package com.frogobox.appsdk.piracy + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AlertDialog +import com.frogobox.appsdk.R +import com.frogobox.appsdk.core.BaseActivity +import com.frogobox.appsdk.databinding.ActivityPiracyBinding +import com.frogobox.sdkutil.piracychecker.allow +import com.frogobox.sdkutil.piracychecker.callback +import com.frogobox.sdkutil.piracychecker.doNotAllow +import com.frogobox.sdkutil.piracychecker.enums.Display +import com.frogobox.sdkutil.piracychecker.enums.InstallerID +import com.frogobox.sdkutil.piracychecker.onError +import com.frogobox.sdkutil.piracychecker.piracyChecker +import com.frogobox.sdkutil.piracychecker.utils.apkSignatures + +class KotlinActivity : BaseActivity() { + + private var piracyCheckerDisplay = Display.DIALOG + + override fun setupViewBinding(): ActivityPiracyBinding { + return ActivityPiracyBinding.inflate(layoutInflater) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setSupportActionBar(binding.toolbar) + + binding.layoutContentMain.radioDisplay.setOnCheckedChangeListener { _, i -> + when (i) { + R.id.radio_dialog -> piracyCheckerDisplay = Display.DIALOG + R.id.radio_activity -> piracyCheckerDisplay = Display.ACTIVITY + } + } + + // Show APK signature + apkSignatures.forEach { Log.e("Signature", it) } + } + + fun toGithub() { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://github.com/javiersantos/piracyChecker"))) + } + + fun verifySignature() { + piracyChecker { + display(piracyCheckerDisplay) + enableSigningCertificates("478yYkKAQF+KST8y4ATKvHkYibo=") // Wrong signature + //enableSigningCertificates("VHZs2aiTBiap/F+AYhYeppy0aF0=") // Right signature + }.start() + } + + fun readSignature() { + val dialogMessage = StringBuilder() + apkSignatures.forEach { + Log.e("Signature", it) + dialogMessage.append("* ").append(it).append("\n") + } + AlertDialog.Builder(this) + .setTitle("APK") + .setMessage(dialogMessage.toString()) + .show() + } + + fun verifyInstallerId() { + piracyChecker { + display(piracyCheckerDisplay) + enableInstallerId(InstallerID.GOOGLE_PLAY) + }.start() + } + + fun verifyUnauthorizedApps() { + piracyChecker { + display(piracyCheckerDisplay) + enableUnauthorizedAppsCheck() + //blockIfUnauthorizedAppUninstalled("license_checker", "block") + }.start() + } + + fun verifyStores() { + piracyChecker { + display(piracyCheckerDisplay) + enableStoresCheck() + }.start() + } + + fun verifyDebug() { + piracyChecker { + display(piracyCheckerDisplay) + enableDebugCheck() + callback { + allow { + // Do something when the user is allowed to use the app + } + doNotAllow { piracyCheckerError, pirateApp -> + // 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}). + + // Additionally, if you enabled the check of pirate apps and/or third-party stores, the 'app' param + // is the app that has been detected on device. App can be null, and when null, it means no pirate app or store was found, + // or you disabled the check for those apps. + // This allows you to let users know the possible reasons why license is been invalid. + } + onError { 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}). + } + } + }.start() + } + + fun verifyEmulator() { + piracyChecker { + display(piracyCheckerDisplay) + enableEmulatorCheck(false) + }.start() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/frogobox/appsdk/piracy/PiracyMainActivity.java b/app/src/main/java/com/frogobox/appsdk/piracy/PiracyMainActivity.java new file mode 100644 index 0000000..abb881f --- /dev/null +++ b/app/src/main/java/com/frogobox/appsdk/piracy/PiracyMainActivity.java @@ -0,0 +1,109 @@ +package com.frogobox.appsdk.piracy; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.View; + +import com.frogobox.appsdk.R; +import com.frogobox.appsdk.core.BaseActivity; +import com.frogobox.appsdk.databinding.ActivityPiracyBinding; +import com.frogobox.sdkutil.piracychecker.PiracyChecker; +import com.frogobox.sdkutil.piracychecker.enums.Display; +import com.frogobox.sdkutil.piracychecker.enums.InstallerID; +import com.frogobox.sdkutil.piracychecker.utils.LibraryUtilsKt; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + +public class PiracyMainActivity extends BaseActivity { + + @NonNull + @Override + public ActivityPiracyBinding setupViewBinding() { + return ActivityPiracyBinding.inflate(getLayoutInflater()); + } + + private Display piracyCheckerDisplay = Display.DIALOG; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setSupportActionBar(getBinding().toolbar); + + getBinding().layoutContentMain.radioDisplay.setOnCheckedChangeListener((radioGroup, i) -> { + switch (i) { + case R.id.radio_dialog -> piracyCheckerDisplay = Display.DIALOG; + case R.id.radio_activity -> piracyCheckerDisplay = Display.ACTIVITY; + default -> piracyCheckerDisplay = Display.DIALOG; + } + }); + + // Show APK signature + for (String signature : LibraryUtilsKt.getApkSignatures(PiracyMainActivity.this)) { + Log.e("Signature", signature); + } + } + + public void toGithub(View view) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/javiersantos/PiracyChecker"))); + } + + public void verifySignature(View view) { + new PiracyChecker(PiracyMainActivity.this) + .display(piracyCheckerDisplay) + .enableSigningCertificates("478yYkKAQF+KST8y4ATKvHkYibo=") // Wrong signature + //.enableSigningCertificates("VHZs2aiTBiap/F+AYhYeppy0aF0=") // Right signature + .start(); + } + + public void readSignature(View view) { + StringBuilder dialogMessage = new StringBuilder(); + for (String signature : LibraryUtilsKt.getApkSignatures(PiracyMainActivity.this)) { + Log.e("Signature", signature); + dialogMessage.append("* ").append(signature).append("\n"); + } + new AlertDialog.Builder(PiracyMainActivity.this) + .setTitle("APK Signatures:") + .setMessage(dialogMessage.toString()) + .show(); + } + + public void verifyInstallerId(View view) { + new PiracyChecker(PiracyMainActivity.this) + .display(piracyCheckerDisplay) + .enableInstallerId(InstallerID.GOOGLE_PLAY) + .start(); + } + + public void verifyUnauthorizedApps(View view) { + new PiracyChecker(PiracyMainActivity.this) + .display(piracyCheckerDisplay) + .enableUnauthorizedAppsCheck() + //.blockIfUnauthorizedAppUninstalled("license_checker", "block") + .start(); + } + + public void verifyStores(View view) { + new PiracyChecker(PiracyMainActivity.this) + .display(piracyCheckerDisplay) + .enableStoresCheck() + .start(); + } + + public void verifyDebug(View view) { + new PiracyChecker(PiracyMainActivity.this) + .display(piracyCheckerDisplay) + .enableDebugCheck() + .start(); + } + + public void verifyEmulator(View view) { + new PiracyChecker(PiracyMainActivity.this) + .display(piracyCheckerDisplay) + .enableEmulatorCheck(false) + .start(); + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_piracy.xml b/app/src/main/res/layout/activity_piracy.xml new file mode 100644 index 0000000..bb85500 --- /dev/null +++ b/app/src/main/res/layout/activity_piracy.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/content_piracy.xml b/app/src/main/res/layout/content_piracy.xml new file mode 100644 index 0000000..d01c6f3 --- /dev/null +++ b/app/src/main/res/layout/content_piracy.xml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0920e06..2803410 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,23 @@ Go to custom notif Go to stack notif Send Custom Layout Notification - + Hello blank fragment + + An Android library that prevents your app from being pirated / cracked using Google Play Licensing (LVL), signing certificate protection and more. + + About + Utils + Show Dialog or Activity + + Dialog + Activity + + Verify using the APK signature + Read APK signature + Verify using the Installer ID + Verify unauthorized apps: Lucky Patcher, Freedom, CreeHack and HappyMod + Verify third-party stores: Aptoide, BlackMart, 1Mobile, GetApk, etc + Verify if app is a debug build + Verify if app is being run in an emulator \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3ca5875..8ef9435 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,4 +18,13 @@ true + + + \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/DependencyGradle.kt b/buildSrc/src/main/kotlin/DependencyGradle.kt index 355dbec..4d2b44b 100644 --- a/buildSrc/src/main/kotlin/DependencyGradle.kt +++ b/buildSrc/src/main/kotlin/DependencyGradle.kt @@ -17,5 +17,6 @@ object DependencyGradle { const val FROGO_PATH_CORE_SDK = ":${ProjectSetting.MODULE_NAME_CORE_SDK}" const val FROGO_PATH_SDK = ":${ProjectSetting.MODULE_NAME_SDK}" + const val FROGO_PATH_SDK_UTIL = ":${ProjectSetting.MODULE_NAME_SDK_UTIL}" } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/ProjectSetting.kt b/buildSrc/src/main/kotlin/ProjectSetting.kt index cd731f2..e0f670f 100644 --- a/buildSrc/src/main/kotlin/ProjectSetting.kt +++ b/buildSrc/src/main/kotlin/ProjectSetting.kt @@ -24,6 +24,7 @@ object ProjectSetting { const val MODULE_NAME_CORE_SDK = "core-sdk" const val MODULE_NAME_SDK = "core-sdk-android" + const val MODULE_NAME_SDK_UTIL = "core-sdk-android-util" // --------------------------------------------------------------------------------------------- diff --git a/core-sdk-android-util/.gitignore b/core-sdk-android-util/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/core-sdk-android-util/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core-sdk-android-util/build.gradle.kts b/core-sdk-android-util/build.gradle.kts new file mode 100644 index 0000000..e4dbe46 --- /dev/null +++ b/core-sdk-android-util/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") + `maven-publish` +} + +android { + + compileSdk = ProjectSetting.PROJECT_COMPILE_SDK + namespace = "com.frogobox.sdkutil.piracychecker" + + defaultConfig { + minSdk = ProjectSetting.PROJECT_MIN_SDK + targetSdk = ProjectSetting.PROJECT_TARGET_SDK + + multiDexEnabled = true + vectorDrawables.useSupportLibrary = true + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + consumerProguardFile("consumer-rules.pro") + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + buildFeatures { + aidl = true + viewBinding = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of("17")) + } + } + +} + +dependencies { + api(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + + api(Androidx.appCompat) + + api(Androidx.activityKtx) + api(Androidx.fragmentKtx) + + api(Androidx.constraintLayout) + api(Androidx.viewPager2) + + api(Androidx.Core.ktx) + + api(Androidx.Lifecycle.runtimeKtx) + api(Androidx.Lifecycle.viewmodelKtx) + api(Androidx.Lifecycle.livedataKtx) + + api(Androidx.preference) + api(Google.material) + +} \ No newline at end of file diff --git a/core-sdk-android-util/proguard-rules.pro b/core-sdk-android-util/proguard-rules.pro new file mode 100644 index 0000000..10e4355 --- /dev/null +++ b/core-sdk-android-util/proguard-rules.pro @@ -0,0 +1,32 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/javiersantos/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Lib +-keep class com.frogobox.sdkutil.** +-dontwarn com.rogobox.sdkutil.** + +# LVL +-keep class com.google.** +-keep class autovalue.shaded.com.google.** +-keep class com.android.vending.billing.** +-keep public class com.android.vending.licensing.ILicensingService + +-dontwarn org.apache.** +-dontwarn com.google.** +-dontwarn autovalue.shaded.com.google.** +-dontwarn com.android.vending.billing.** \ No newline at end of file diff --git a/core-sdk-android-util/src/main/AndroidManifest.xml b/core-sdk-android-util/src/main/AndroidManifest.xml new file mode 100644 index 0000000..562fea5 --- /dev/null +++ b/core-sdk-android-util/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/core-sdk-android-util/src/main/aidl/com/android/vending/licensing/ILicenseResultListener.aidl b/core-sdk-android-util/src/main/aidl/com/android/vending/licensing/ILicenseResultListener.aidl new file mode 100644 index 0000000..869cb16 --- /dev/null +++ b/core-sdk-android-util/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/core-sdk-android-util/src/main/aidl/com/android/vending/licensing/ILicensingService.aidl b/core-sdk-android-util/src/main/aidl/com/android/vending/licensing/ILicensingService.aidl new file mode 100644 index 0000000..9541a20 --- /dev/null +++ b/core-sdk-android-util/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/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/AESObfuscator.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/AESObfuscator.java new file mode 100644 index 0000000..63035d8 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/AESObfuscator.java @@ -0,0 +1,106 @@ +/* + * 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.frogobox.sdkutil.licensing; + +import com.frogobox.sdkutil.licensing.util.Base64; +import com.frogobox.sdkutil.licensing.util.Base64DecoderException; + +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.spec.KeySpec; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * An Obfuscator that uses AES to encrypt data. + */ +public class AESObfuscator implements Obfuscator { + private static final String UTF8 = "UTF-8"; + 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.frogobox.sdkutil.licensing.AESObfuscator-1|"; + + private Cipher mEncryptor; + private Cipher mDecryptor; + + /** + * @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. + */ + 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); + SecretKey tmp = factory.generateSecret(keySpec); + SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); + mEncryptor = Cipher.getInstance(CIPHER_ALGORITHM); + mEncryptor.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(IV)); + mDecryptor = Cipher.getInstance(CIPHER_ALGORITHM); + mDecryptor.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(IV)); + } catch (GeneralSecurityException e) { + // This can't happen on a compatible Android device. + throw new RuntimeException("Invalid environment", e); + } + } + + public String obfuscate(String original, String key) { + if (original == null) { + return null; + } + try { + // Header is appended as an integrity check + return Base64.encode(mEncryptor.doFinal((header + key + original).getBytes(UTF8))); + } catch (UnsupportedEncodingException | GeneralSecurityException e) { + throw new RuntimeException("Invalid environment", e); + } + } + + public String unobfuscate(String obfuscated, String key) throws ValidationException { + if (obfuscated == null) { + return null; + } + try { + 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); + 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 | IllegalBlockSizeException | BadPaddingException e) { + throw new ValidationException(e.getMessage() + ":" + obfuscated); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Invalid environment", e); + } + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/APKExpansionPolicy.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/APKExpansionPolicy.java new file mode 100644 index 0000000..692abcd --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/APKExpansionPolicy.java @@ -0,0 +1,375 @@ +package com.frogobox.sdkutil.licensing; + +/* + * Copyright (C) 2012 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. + */ + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.frogobox.sdkutil.licensing.util.URIQueryDecoder; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +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 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.frogobox.sdkutil.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"; + private static final String PREF_MAX_RETRIES = "maxRetries"; + private static final String PREF_RETRY_COUNT = "retryCount"; + private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; + 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; + private long mRetryCount; + 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<>(); + + /** + * @param context + * The context for the current application + * @param obfuscator + * An obfuscator to be used with preferences. + */ + public APKExpansionPolicy(Context context, Obfuscator obfuscator) { + // Import old values + SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); + mPreferences = new PreferenceObfuscator(sp, obfuscator); + mLastResponse = Integer.parseInt( + mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(RETRY))); + mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, + DEFAULT_VALIDITY_TIMESTAMP)); + mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); + mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); + mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); + } + + /** + * 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(RETRY)); + setRetryUntil(DEFAULT_RETRY_UNTIL); + setMaxRetries(DEFAULT_MAX_RETRIES); + setRetryCount(Long.parseLong(DEFAULT_RETRY_COUNT)); + setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); + mPreferences.commit(); + } + + /** + * 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 + */ + public void processServerResponse(int response, + ResponseData rawData) { + + // Update retry counter + if (response != RETRY) { + setRetryCount(0); + } else { + setRetryCount(mRetryCount + 1); + } + + if (response == LICENSED) { + // Update server policy data + Map extras = decodeExtras(rawData.extra); + mLastResponse = response; + setValidityTimestamp(Long.toString(System.currentTimeMillis() + MILLIS_PER_MINUTE)); + Set keys = extras.keySet(); + for (String key : keys) { + if (key.equals("VT")) { + setValidityTimestamp(extras.get(key)); + } else if (key.equals("GT")) { + setRetryUntil(extras.get(key)); + } else if (key.equals("GR")) { + setMaxRetries(extras.get(key)); + } else if (key.startsWith("FILE_URL")) { + int index = Integer.parseInt(key.substring("FILE_URL".length())) - 1; + setExpansionURL(index, extras.get(key)); + } else if (key.startsWith("FILE_NAME")) { + int index = Integer.parseInt(key.substring("FILE_NAME".length())) - 1; + setExpansionFileName(index, extras.get(key)); + } else if (key.startsWith("FILE_SIZE")) { + int index = Integer.parseInt(key.substring("FILE_SIZE".length())) - 1; + setExpansionFileSize(index, Long.parseLong(extras.get(key))); + } + } + } else if (response == NOT_LICENSED) { + // Clear out stale policy data + setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); + setRetryUntil(DEFAULT_RETRY_UNTIL); + setMaxRetries(DEFAULT_MAX_RETRIES); + } + + setLastResponse(response); + mPreferences.commit(); + } + + /** + * 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) { + mLastResponseTime = System.currentTimeMillis(); + mLastResponse = 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. + * + * @param c + * the new retry count + */ + private void setRetryCount(long c) { + mRetryCount = c; + mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); + } + + 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. + * + * @param validityTimestamp + * the VT string received + */ + private void setValidityTimestamp(String validityTimestamp) { + Long lValidityTimestamp; + try { + lValidityTimestamp = Long.parseLong(validityTimestamp); + } catch (NumberFormatException e) { + // No response or not parseable, expire in one minute. + Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); + lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; + validityTimestamp = Long.toString(lValidityTimestamp); + } + + mValidityTimestamp = lValidityTimestamp; + mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); + } + + 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. + * + * @param retryUntil + * the GT string received + */ + private void setRetryUntil(String retryUntil) { + Long lRetryUntil; + try { + lRetryUntil = Long.parseLong(retryUntil); + } catch (NumberFormatException e) { + // No response or not parseable, expire immediately + Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); + retryUntil = "0"; + lRetryUntil = 0L; + } + + mRetryUntil = lRetryUntil; + mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); + } + + 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. + * + * @param maxRetries + * the GR string received + */ + private void setMaxRetries(String maxRetries) { + Long lMaxRetries; + try { + lMaxRetries = Long.parseLong(maxRetries); + } catch (NumberFormatException e) { + // No response or not parseable, expire immediately + Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); + maxRetries = "0"; + lMaxRetries = 0L; + } + + mMaxRetries = lMaxRetries; + mPreferences.putString(PREF_MAX_RETRIES, maxRetries); + } + + /** + * 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() { + return mExpansionURLs.size(); + } + + /** + * 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()) { + return mExpansionURLs.elementAt(index); + } + return null; + } + + /** + * 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()) { + mExpansionURLs.setSize(index + 1); + } + mExpansionURLs.set(index, URL); + } + + public String getExpansionFileName(int index) { + if (index < mExpansionFileNames.size()) { + return mExpansionFileNames.elementAt(index); + } + return null; + } + + public void setExpansionFileName(int index, String name) { + if (index >= mExpansionFileNames.size()) { + mExpansionFileNames.setSize(index + 1); + } + mExpansionFileNames.set(index, name); + } + + public long getExpansionFileSize(int index) { + if (index < mExpansionFileSizes.size()) { + return mExpansionFileSizes.elementAt(index); + } + return -1; + } + + public void setExpansionFileSize(int index, long size) { + if (index >= mExpansionFileSizes.size()) { + mExpansionFileSizes.setSize(index + 1); + } + mExpansionFileSizes.set(index, 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.
+ */ + public boolean allowAccess() { + long ts = System.currentTimeMillis(); + if (mLastResponse == LICENSED) { + // Check if the LICENSED response occurred within the validity + // timeout. + if (ts <= mValidityTimestamp) { + // Cached LICENSED response is still valid. + return true; + } + } else if (mLastResponse == RETRY && + 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); + } + return false; + } + + private Map decodeExtras(String extras) { + Map results = new HashMap<>(); + try { + URI rawExtras = new URI("?" + extras); + URIQueryDecoder.DecodeQuery(rawExtras, results); + } catch (URISyntaxException e) { + Log.w(TAG, "Invalid syntax error while decoding extras data from server."); + } + return results; + } + +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/DeviceLimiter.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/DeviceLimiter.java new file mode 100644 index 0000000..efdd33a --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/DeviceLimiter.java @@ -0,0 +1,43 @@ +/* + * 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.frogobox.sdkutil.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. + */ +public interface DeviceLimiter { + + /** + * Checks if this device is allowed to use the given user's license. + * + * @param userId + * the user whose license the server responded with + * + * @return LICENSED if the device is allowed, NOT_LICENSED if not, RETRY if an error occurs + */ + int isDeviceAllowed(String userId); +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryChecker.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryChecker.java new file mode 100644 index 0000000..c569d30 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryChecker.java @@ -0,0 +1,396 @@ +/* + * 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.frogobox.sdkutil.licensing; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.RemoteException; +import android.provider.Settings.Secure; +import android.util.Log; + +import com.android.vending.licensing.ILicenseResultListener; +import com.android.vending.licensing.ILicensingService; +import com.frogobox.sdkutil.licensing.util.Base64; +import com.frogobox.sdkutil.licensing.util.Base64DecoderException; + +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.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; + +/** + * Client library for Google Play license verifications.

The LibraryChecker 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 LibraryChecker implements ServiceConnection { + private static final String TAG = "LibraryChecker"; + + private static final String KEY_FACTORY_ALGORITHM = "RSA"; + + // Timeout value (in milliseconds) for calls to service. + private static final int TIMEOUT_MS = 10 * 1000; + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final boolean DEBUG_LICENSE_ERROR = false; + 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. + */ + private Handler mHandler; + + /** + * @param context + * a Context + * @param policy + * implementation of Policy + * @param encodedPublicKey + * Base64-encoded RSA public key + * + * @throws IllegalArgumentException + * if encodedPublicKey is invalid + */ + public LibraryChecker(Context context, Policy policy, String encodedPublicKey) { + mContext = context; + mPolicy = policy; + mPublicKey = generatePublicKey(encodedPublicKey); + mPackageName = mContext.getPackageName(); + mVersionCode = getVersionCode(context, mPackageName); + HandlerThread handlerThread = new HandlerThread("background thread"); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper()); + } + + /** + * 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 + */ + private static PublicKey generatePublicKey(String encodedPublicKey) { + try { + byte[] decodedKey = Base64.decode(encodedPublicKey); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + + return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); + } catch (NoSuchAlgorithmException e) { + // This won't happen in an Android-compatible environment. + throw new RuntimeException(e); + } catch (Base64DecoderException e) { + Log.e(TAG, "Could not decode from Base64."); + throw new IllegalArgumentException(e); + } catch (InvalidKeySpecException e) { + Log.e(TAG, "Invalid key specification."); + throw new IllegalArgumentException(e); + } + } + + /** + * 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(LibraryCheckerCallback callback) { + // If we have a valid recent LICENSED response, we can skip asking + // Market. + if (mPolicy.allowAccess()) { + Log.i(TAG, "Using cached license response"); + callback.allow(Policy.LICENSED); + } else { + LibraryValidator validator = new LibraryValidator(mPolicy, new NullDeviceLimiter(), + callback, generateNonce(), + mPackageName, mVersionCode); + + if (mService == null) { + Log.i(TAG, "Binding to licensing service."); + try { + boolean bindResult = mContext + .bindService( + new Intent( + new String( + // 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 { + Log.e(TAG, "Could not bind to service."); + handleServiceConnectionError(validator); + } + } catch (SecurityException e) { + callback.applicationError(LibraryCheckerCallback.ERROR_MISSING_PERMISSION); + } catch (Base64DecoderException e) { + e.printStackTrace(); + } + } else { + mPendingChecks.offer(validator); + runChecks(); + } + } + } + + private void runChecks() { + LibraryValidator validator; + while ((validator = mPendingChecks.poll()) != null) { + try { + Log.i(TAG, "Calling checkLicense on service for " + validator.getPackageName()); + mService.checkLicense( + validator.getNonce(), validator.getPackageName(), + new ResultListener(validator)); + mChecksInProgress.add(validator); + } catch (RemoteException e) { + Log.w(TAG, "RemoteException in checkLicense call.", e); + handleServiceConnectionError(validator); + } + } + } + + public synchronized void finishAllChecks() { + for (LibraryValidator validator : mChecksInProgress) { + try { + finishCheck(validator); + } catch (Exception ignored) { + } + } + for (LibraryValidator validator : mPendingChecks) { + try { + mPendingChecks.remove(validator); + } catch (Exception ignored) { + } + } + } + + private synchronized void finishCheck(LibraryValidator validator) { + mChecksInProgress.remove(validator); + if (mChecksInProgress.isEmpty()) { + cleanupService(); + } + } + + 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(LibraryValidator 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(); + } + + public 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 LibraryValidator mValidator; + private Runnable mOnTimeout; + + public ResultListener(LibraryValidator validator) { + mValidator = validator; + mOnTimeout = new Runnable() { + public void run() { + Log.i(TAG, "Check timed out."); + handleServiceConnectionError(mValidator); + finishCheck(mValidator); + } + }; + startTimeout(); + } + + // 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) { + mHandler.post(new Runnable() { + public void run() { + Log.i(TAG, "Received response."); + // Make sure it hasn't already timed out. + if (mChecksInProgress.contains(mValidator)) { + clearTimeout(); + mValidator.check(mPublicKey, responseCode, signedData, + Calendar.getInstance(), signature); + finishCheck(mValidator); + } + if (DEBUG_LICENSE_ERROR) { + boolean logResponse; + String stringError = null; + switch (responseCode) { + case ERROR_CONTACTING_SERVER: + logResponse = true; + stringError = "ERROR_CONTACTING_SERVER"; + break; + case ERROR_INVALID_PACKAGE_NAME: + logResponse = true; + stringError = "ERROR_INVALID_PACKAGE_NAME"; + break; + case ERROR_NON_MATCHING_UID: + logResponse = true; + stringError = "ERROR_NON_MATCHING_UID"; + break; + default: + logResponse = false; + } + + if (logResponse) { + String android_id = Secure.getString(mContext.getContentResolver(), + Secure.ANDROID_ID); + Date date = new Date(); + Log.d(TAG, "Server Failure: " + stringError); + Log.d(TAG, "Android ID: " + android_id); + SimpleDateFormat df = new SimpleDateFormat("dd MMM yyyy HH:mm:ss"); + String asGmt = df.format(date) + " GMT"; + Log.d(TAG, "Time: " + asGmt); + } + } + + } + }); + } + + private void startTimeout() { + Log.i(TAG, "Start monitoring timeout."); + mHandler.postDelayed(mOnTimeout, TIMEOUT_MS); + } + + private void clearTimeout() { + Log.i(TAG, "Clearing timeout."); + mHandler.removeCallbacks(mOnTimeout); + } + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryCheckerCallback.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryCheckerCallback.java new file mode 100644 index 0000000..1eb6bd8 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryCheckerCallback.java @@ -0,0 +1,66 @@ +/* + * 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.frogobox.sdkutil.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. + */ +public interface LibraryCheckerCallback { + + /** + * 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) + */ + 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) + */ + void dontAllow(int reason); + + /** + * Error in application code. Caller did not call or set up license checker correctly. Should be + * considered fatal. + */ + void applicationError(int errorCode); + +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryValidator.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryValidator.java new file mode 100644 index 0000000..def5c76 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/LibraryValidator.java @@ -0,0 +1,232 @@ +/* + * 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.frogobox.sdkutil.licensing; + +import android.text.TextUtils; +import android.util.Log; + +import com.frogobox.sdkutil.licensing.util.Base64; +import com.frogobox.sdkutil.licensing.util.Base64DecoderException; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Calendar; + +/** + * Contains data related to a licensing request and methods to check and process the response. + */ +class LibraryValidator { + private static final String TAG = "LibraryValidator"; + + // Server response codes. + private static final int LICENSED = 0x0; + private static final int NOT_LICENSED = 0x1; + private static final int LICENSED_OLD_KEY = 0x2; + private static final int ERROR_NOT_MARKET_MANAGED = 0x3; + private static final int ERROR_SERVER_FAILURE = 0x4; + private static final int ERROR_OVER_QUOTA = 0x5; + + 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 LibraryCheckerCallback mCallback; + private final int mNonce; + private final String mPackageName; + private final String mVersionCode; + private final DeviceLimiter mDeviceLimiter; + + LibraryValidator(Policy policy, DeviceLimiter deviceLimiter, LibraryCheckerCallback callback, + int nonce, String packageName, String versionCode) { + mPolicy = policy; + mDeviceLimiter = deviceLimiter; + mCallback = callback; + mNonce = nonce; + mPackageName = packageName; + mVersionCode = versionCode; + } + + public LibraryCheckerCallback getCallback() { + return mCallback; + } + + public int getNonce() { + return mNonce; + } + + public String getPackageName() { + return mPackageName; + } + + /** + * Verifies the response from server and calls appropriate callback method. + * + * @param publicKey + * public key associated with the developer account + * @param responseCode + * server response code + * @param signedData + * signed data from server + * @param signature + * server signature + */ + public void check(PublicKey publicKey, int responseCode, String signedData, Calendar calendar, + String signature) { + String userId = null; + // Skip signature check for unsuccessful requests + ResponseData data = null; + + if (calendar == null) { + handleInvalidResponse(); + } else if (responseCode == LICENSED || responseCode == NOT_LICENSED || + responseCode == LICENSED_OLD_KEY) { + // 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()); + + if (!sig.verify(Base64.decode(signature))) { + Log.e(TAG, "Signature verification failed."); + handleInvalidResponse(); + return; + } + } catch (NoSuchAlgorithmException | SignatureException e) { + // This can't happen on an Android compatible device. + throw new RuntimeException(e); + } catch (InvalidKeyException e) { + handleApplicationError(LibraryCheckerCallback.ERROR_INVALID_PUBLIC_KEY); + return; + } catch (Base64DecoderException e) { + Log.e(TAG, "Could not Base64-decode signature."); + handleInvalidResponse(); + return; + } + + // Parse and validate response. + try { + data = ResponseData.parse(signedData); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not parse response."); + handleInvalidResponse(); + return; + } + + if (data.responseCode != responseCode) { + Log.e(TAG, "Response codes don't match."); + handleInvalidResponse(); + return; + } + + if (data.nonce != mNonce) { + Log.e(TAG, "Nonce doesn't match."); + handleInvalidResponse(); + return; + } + + if (!data.packageName.equals(mPackageName)) { + Log.e(TAG, "Package name doesn't match."); + handleInvalidResponse(); + return; + } + + if (!data.versionCode.equals(mVersionCode)) { + Log.e(TAG, "Version codes don't match."); + handleInvalidResponse(); + return; + } + + // Application-specific user identifier. + userId = data.userId; + if (TextUtils.isEmpty(userId)) { + Log.e(TAG, "User identifier is empty."); + handleInvalidResponse(); + return; + } + } + + switch (responseCode) { + case LICENSED: + case LICENSED_OLD_KEY: + int limiterResponse = mDeviceLimiter.isDeviceAllowed(userId); + handleResponse(limiterResponse, data); + break; + case NOT_LICENSED: + handleResponse(Policy.NOT_LICENSED, data); + break; + case ERROR_CONTACTING_SERVER: + Log.w(TAG, "Error contacting licensing server."); + handleResponse(Policy.RETRY, data); + break; + case ERROR_SERVER_FAILURE: + Log.w(TAG, "An error has occurred on the licensing server."); + handleResponse(Policy.RETRY, data); + break; + case ERROR_OVER_QUOTA: + Log.w(TAG, "Licensing server is refusing to talk to this device, over quota."); + handleResponse(Policy.RETRY, data); + break; + case ERROR_INVALID_PACKAGE_NAME: + handleApplicationError(LibraryCheckerCallback.ERROR_INVALID_PACKAGE_NAME); + break; + case ERROR_NON_MATCHING_UID: + handleApplicationError(LibraryCheckerCallback.ERROR_NON_MATCHING_UID); + break; + case ERROR_NOT_MARKET_MANAGED: + handleApplicationError(LibraryCheckerCallback.ERROR_NOT_MARKET_MANAGED); + break; + default: + Log.e(TAG, "Unknown response code for license check."); + handleInvalidResponse(); + } + } + + /** + * Confers with policy and calls appropriate callback method. + */ + private void handleResponse(int response, ResponseData rawData) { + // Update policy data and increment retry counter (if needed) + mPolicy.processServerResponse(response, rawData); + + // Given everything we know, including cached data, ask the policy if we should grant + // access. + if (mPolicy.allowAccess()) { + mCallback.allow(response); + } else { + mCallback.dontAllow(response); + } + } + + private void handleApplicationError(int code) { + mCallback.applicationError(code); + } + + private void handleInvalidResponse() { + mCallback.dontAllow(Policy.NOT_LICENSED); + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/NullDeviceLimiter.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/NullDeviceLimiter.java new file mode 100644 index 0000000..75db7fd --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/NullDeviceLimiter.java @@ -0,0 +1,30 @@ +/* + * 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.frogobox.sdkutil.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. + */ +public class NullDeviceLimiter implements DeviceLimiter { + + public int isDeviceAllowed(String userId) { + return Policy.LICENSED; + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/Obfuscator.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/Obfuscator.java new file mode 100644 index 0000000..771537b --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/Obfuscator.java @@ -0,0 +1,55 @@ +/* + * 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.frogobox.sdkutil.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. + */ +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. + * + * @return A transformed version of the original data. + */ + String obfuscate(String original, String key); + + /** + * Undo the transformation applied to data by the obfuscate() method. + * + * @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/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/Policy.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/Policy.java new file mode 100644 index 0000000..e558aa7 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/Policy.java @@ -0,0 +1,60 @@ +/* + * 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.frogobox.sdkutil.licensing; + +/** + * Policy used by {@link LibraryChecker} to determine whether a user should have access to the + * application. + */ +public interface Policy { + + /** + * Change these values to make it more difficult for tools to automatically + * strip LVL protection from your APK. + */ + + /** + * LICENSED means that the server returned back a valid license response + */ + int LICENSED = 0x0B8A; + /** + * NOT_LICENSED means that the server returned back a valid license response that indicated that + * the user definitively is not licensed + */ + int NOT_LICENSED = 0x01B3; + /** + * RETRY means that the license response was unable to be determined --- perhaps as a result of + * faulty networking + */ + int RETRY = 0x0C48; + + /** + * 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 + */ + void processServerResponse(int response, ResponseData rawData); + + /** + * Check if the user should be allowed access to the application. + */ + boolean allowAccess(); +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/PreferenceObfuscator.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/PreferenceObfuscator.java new file mode 100644 index 0000000..98c4823 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/PreferenceObfuscator.java @@ -0,0 +1,81 @@ +/* + * 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.frogobox.sdkutil.licensing; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.util.Log; + +/** + * An wrapper for SharedPreferences that transparently performs data obfuscation. + */ +public class PreferenceObfuscator { + + private static final String TAG = "PreferenceObfuscator"; + + private final SharedPreferences mPreferences; + private final Obfuscator mObfuscator; + private SharedPreferences.Editor mEditor; + + /** + * Constructor. + * + * @param sp + * A SharedPreferences instance provided by the system. + * @param o + * The Obfuscator to use when reading or writing data. + */ + public PreferenceObfuscator(SharedPreferences sp, Obfuscator o) { + mPreferences = sp; + mObfuscator = o; + mEditor = null; + } + + @SuppressLint("CommitPrefEdits") + public void putString(String key, String value) { + if (mEditor == null) { + mEditor = mPreferences.edit(); + } + String obfuscatedValue = mObfuscator.obfuscate(value, key); + mEditor.putString(key, obfuscatedValue); + } + + public String getString(String key, String defValue) { + String result; + String value = mPreferences.getString(key, null); + if (value != null) { + try { + result = mObfuscator.unobfuscate(value, key); + } catch (ValidationException e) { + // Unable to unobfuscate, data corrupt or tampered + Log.w(TAG, "Validation error while reading preference: " + key); + result = defValue; + } + } else { + // Preference not found + result = defValue; + } + return result; + } + + public void commit() { + if (mEditor != null) { + mEditor.commit(); + mEditor = null; + } + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ResponseData.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ResponseData.java new file mode 100644 index 0000000..cfd4941 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ResponseData.java @@ -0,0 +1,87 @@ +/* + * 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.frogobox.sdkutil.licensing; + +import android.text.TextUtils; + +import java.util.regex.Pattern; + +/** + * ResponseData from licensing server. + */ +public class ResponseData { + + public int responseCode; + public int nonce; + public String packageName; + public String versionCode; + public String userId; + public long timestamp; + /** + * Response-specific data. + */ + public String extra; + + /** + * Parses response string into ResponseData. + * + * @param responseData + * response data string + * + * @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); + } + + String[] fields = TextUtils.split(mainData, Pattern.quote("|")); + if (fields.length < 6) { + throw new IllegalArgumentException("Wrong number of fields."); + } + + ResponseData data = new ResponseData(); + data.extra = extraData; + data.responseCode = Integer.parseInt(fields[0]); + data.nonce = Integer.parseInt(fields[1]); + data.packageName = fields[2]; + data.versionCode = fields[3]; + // Application-specific user identifier. + data.userId = fields[4]; + data.timestamp = Long.parseLong(fields[5]); + + return data; + } + + @Override + public String toString() { + return TextUtils.join("|", new Object[]{ + responseCode, nonce, packageName, versionCode, + userId, timestamp + }); + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ServerManagedPolicy.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ServerManagedPolicy.java new file mode 100644 index 0000000..72d37d7 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ServerManagedPolicy.java @@ -0,0 +1,264 @@ +/* + * 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.frogobox.sdkutil.licensing; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.frogobox.sdkutil.licensing.util.URIQueryDecoder; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +/** + * 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.frogobox.sdkutil.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"; + private static final String PREF_MAX_RETRIES = "maxRetries"; + private static final String PREF_RETRY_COUNT = "retryCount"; + private static final String DEFAULT_VALIDITY_TIMESTAMP = "0"; + 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; + private long mRetryCount; + private long mLastResponseTime = 0; + private int mLastResponse; + private PreferenceObfuscator mPreferences; + + /** + * @param context + * The context for the current application + * @param obfuscator + * An obfuscator to be used with preferences. + */ + public ServerManagedPolicy(Context context, Obfuscator obfuscator) { + // Import old values + SharedPreferences sp = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE); + mPreferences = new PreferenceObfuscator(sp, obfuscator); + mLastResponse = Integer.parseInt( + mPreferences.getString(PREF_LAST_RESPONSE, Integer.toString(RETRY))); + mValidityTimestamp = Long.parseLong(mPreferences.getString(PREF_VALIDITY_TIMESTAMP, + DEFAULT_VALIDITY_TIMESTAMP)); + mRetryUntil = Long.parseLong(mPreferences.getString(PREF_RETRY_UNTIL, DEFAULT_RETRY_UNTIL)); + mMaxRetries = Long.parseLong(mPreferences.getString(PREF_MAX_RETRIES, DEFAULT_MAX_RETRIES)); + mRetryCount = Long.parseLong(mPreferences.getString(PREF_RETRY_COUNT, DEFAULT_RETRY_COUNT)); + } + + /** + * 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 + */ + public void processServerResponse(int response, ResponseData rawData) { + + // Update retry counter + if (response != RETRY) { + setRetryCount(0); + } else { + setRetryCount(mRetryCount + 1); + } + + if (response == LICENSED) { + // Update server policy data + Map extras = decodeExtras(rawData.extra); + mLastResponse = response; + setValidityTimestamp(extras.get("VT")); + setRetryUntil(extras.get("GT")); + setMaxRetries(extras.get("GR")); + } else if (response == NOT_LICENSED) { + // Clear out stale policy data + setValidityTimestamp(DEFAULT_VALIDITY_TIMESTAMP); + setRetryUntil(DEFAULT_RETRY_UNTIL); + setMaxRetries(DEFAULT_MAX_RETRIES); + } + + setLastResponse(response); + mPreferences.commit(); + } + + /** + * 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) { + mLastResponseTime = System.currentTimeMillis(); + mLastResponse = 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. + * + * @param c + * the new retry count + */ + private void setRetryCount(long c) { + mRetryCount = c; + mPreferences.putString(PREF_RETRY_COUNT, Long.toString(c)); + } + + 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. + * + * @param validityTimestamp + * the VT string received + */ + private void setValidityTimestamp(String validityTimestamp) { + Long lValidityTimestamp; + try { + lValidityTimestamp = Long.parseLong(validityTimestamp); + } catch (NumberFormatException e) { + // No response or not parsable, expire in one minute. + Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute"); + lValidityTimestamp = System.currentTimeMillis() + MILLIS_PER_MINUTE; + validityTimestamp = Long.toString(lValidityTimestamp); + } + + mValidityTimestamp = lValidityTimestamp; + mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp); + } + + 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. + * + * @param retryUntil + * the GT string received + */ + private void setRetryUntil(String retryUntil) { + Long lRetryUntil; + try { + lRetryUntil = Long.parseLong(retryUntil); + } catch (NumberFormatException e) { + // No response or not parsable, expire immediately + Log.w(TAG, "License retry timestamp (GT) missing, grace period disabled"); + retryUntil = "0"; + lRetryUntil = 0L; + } + + mRetryUntil = lRetryUntil; + mPreferences.putString(PREF_RETRY_UNTIL, retryUntil); + } + + 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. + * + * @param maxRetries + * the GR string received + */ + private void setMaxRetries(String maxRetries) { + Long lMaxRetries; + try { + lMaxRetries = Long.parseLong(maxRetries); + } catch (NumberFormatException e) { + // No response or not parsable, expire immediately + Log.w(TAG, "Licence retry count (GR) missing, grace period disabled"); + maxRetries = "0"; + lMaxRetries = 0L; + } + + mMaxRetries = lMaxRetries; + mPreferences.putString(PREF_MAX_RETRIES, maxRetries); + } + + /** + * {@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(); + if (mLastResponse == LICENSED) { + // Check if the LICENSED response occurred within the validity timeout. + if (ts <= mValidityTimestamp) { + // Cached LICENSED response is still valid. + return true; + } + } else if (mLastResponse == RETRY && + 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); + } + return false; + } + + private Map decodeExtras(String extras) { + Map results = new HashMap<>(); + try { + URI rawExtras = new URI("?" + extras); + URIQueryDecoder.DecodeQuery(rawExtras, results); + } catch (URISyntaxException e) { + Log.w(TAG, "Invalid syntax error while decoding extras data from server."); + } + return results; + } + +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/StrictPolicy.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/StrictPolicy.java new file mode 100644 index 0000000..720c580 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/StrictPolicy.java @@ -0,0 +1,59 @@ +/* + * 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.frogobox.sdkutil.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. + */ +public class StrictPolicy implements Policy { + + private int mLastResponse; + + public StrictPolicy() { + // Set default policy. This will force the application to check the policy on launch. + mLastResponse = Policy.RETRY; + } + + /** + * 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 + */ + public void processServerResponse(int response, ResponseData rawData) { + mLastResponse = response; + } + + /** + * {@inheritDoc} + *

+ * 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/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ValidationException.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ValidationException.java new file mode 100644 index 0000000..6341a30 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/ValidationException.java @@ -0,0 +1,33 @@ +/* + * 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.frogobox.sdkutil.licensing; + +/** + * 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(); + } + + public ValidationException(String s) { + super(s); + } +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/Base64.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/Base64.java new file mode 100644 index 0000000..c28a9a6 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/Base64.java @@ -0,0 +1,630 @@ +// Portions copyright 2002, Google, Inc. +// +// 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.frogobox.sdkutil.licensing.util; + +// This code was converted from code at http://iharder.sourceforge.net/base64/ +// Lots of extraneous features were removed. +/* The original code said: + *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit + * http://iharder.net/xmlizable + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rharder@usa.net + * @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. + *

+ *

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 + /* ,-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 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -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 + /* ,-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 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -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; + + /** + * 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) && outLen > 0) { + if (outBuff[outLen - 1] != '=') { + break; + } + outLen -= 1; + } + + 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 + */ + @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; + } + } // 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 + */ + 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)"); + } + } + + // 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; + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/Base64DecoderException.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/Base64DecoderException.java new file mode 100644 index 0000000..a4f8a13 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/Base64DecoderException.java @@ -0,0 +1,32 @@ +// Copyright 2002, Google, Inc. +// +// 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.frogobox.sdkutil.licensing.util; + +/** + * Exception thrown when encountering an invalid Base64 input character. + * + * @author nelson + */ +public class Base64DecoderException extends Exception { + private static final long serialVersionUID = 1L; + + public Base64DecoderException() { + super(); + } + + public Base64DecoderException(String s) { + super(s); + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/URIQueryDecoder.java b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/URIQueryDecoder.java new file mode 100644 index 0000000..18b889c --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/licensing/util/URIQueryDecoder.java @@ -0,0 +1,62 @@ +/* + * 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.frogobox.sdkutil.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 diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/Extensions.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/Extensions.kt new file mode 100644 index 0000000..878be69 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/Extensions.kt @@ -0,0 +1,48 @@ +@file:Suppress("unused") + +package com.frogobox.sdkutil.piracychecker + +import android.content.Context +import androidx.fragment.app.Fragment +import com.frogobox.sdkutil.piracychecker.callbacks.AllowCallback +import com.frogobox.sdkutil.piracychecker.callbacks.DoNotAllowCallback +import com.frogobox.sdkutil.piracychecker.callbacks.OnErrorCallback +import com.frogobox.sdkutil.piracychecker.callbacks.PiracyCheckerCallbacksDSL +import com.frogobox.sdkutil.piracychecker.enums.PiracyCheckerError +import com.frogobox.sdkutil.piracychecker.enums.PirateApp + +fun Context.piracyChecker(builder: PiracyChecker.() -> Unit): PiracyChecker { + val checker = PiracyChecker(this) + checker.builder() + return checker +} + +fun Fragment.piracyChecker(builder: PiracyChecker.() -> Unit): PiracyChecker = + activity?.piracyChecker(builder) ?: requireContext().piracyChecker(builder) + +inline fun PiracyChecker.allow(crossinline allow: () -> Unit = {}) = apply { + allowCallback(object : AllowCallback { + override fun allow() = allow() + }) +} + +inline fun PiracyChecker.doNotAllow(crossinline doNotAllow: (PiracyCheckerError, PirateApp?) -> Unit = { _, _ -> }) = + apply { + doNotAllowCallback(object : DoNotAllowCallback { + override fun doNotAllow(error: PiracyCheckerError, app: PirateApp?) = + doNotAllow(error, app) + }) + } + +inline fun PiracyChecker.onError(crossinline onError: (PiracyCheckerError) -> Unit = {}) = apply { + onErrorCallback(object : OnErrorCallback { + override fun onError(error: PiracyCheckerError) { + super.onError(error) + onError(error) + } + }) +} + +fun PiracyChecker.callback(callbacks: PiracyCheckerCallbacksDSL.() -> Unit) { + PiracyCheckerCallbacksDSL(this).callbacks() +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/PiracyChecker.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/PiracyChecker.kt new file mode 100644 index 0000000..1e25441 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/PiracyChecker.kt @@ -0,0 +1,485 @@ +package com.frogobox.sdkutil.piracychecker + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.provider.Settings +import android.util.Log +import androidx.annotation.ColorRes +import androidx.annotation.LayoutRes +import androidx.annotation.StringRes +import com.frogobox.sdkutil.licensing.AESObfuscator +import com.frogobox.sdkutil.licensing.LibraryChecker +import com.frogobox.sdkutil.licensing.LibraryCheckerCallback +import com.frogobox.sdkutil.licensing.ServerManagedPolicy +import com.frogobox.sdkutil.piracychecker.R +import com.frogobox.sdkutil.piracychecker.activities.LicenseActivity +import com.frogobox.sdkutil.piracychecker.callbacks.AllowCallback +import com.frogobox.sdkutil.piracychecker.callbacks.DoNotAllowCallback +import com.frogobox.sdkutil.piracychecker.callbacks.OnErrorCallback +import com.frogobox.sdkutil.piracychecker.callbacks.PiracyCheckerCallback +import com.frogobox.sdkutil.piracychecker.enums.AppType +import com.frogobox.sdkutil.piracychecker.enums.Display +import com.frogobox.sdkutil.piracychecker.enums.InstallerID +import com.frogobox.sdkutil.piracychecker.enums.PiracyCheckerError +import com.frogobox.sdkutil.piracychecker.enums.PirateApp +import com.frogobox.sdkutil.piracychecker.utils.SaltUtils +import com.frogobox.sdkutil.piracychecker.utils.getPirateApp +import com.frogobox.sdkutil.piracychecker.utils.isDebug +import com.frogobox.sdkutil.piracychecker.utils.isInEmulator +import com.frogobox.sdkutil.piracychecker.utils.verifyInstallerId +import com.frogobox.sdkutil.piracychecker.utils.verifySigningCertificates +import java.util.ArrayList +import java.util.Arrays + +// Library configuration/customizations +@Suppress("unused") +@SuppressLint("HardwareIds") +class PiracyChecker( + private var context: Context?, + var unlicensedDialogTitle: String? = + context?.getString(R.string.app_unlicensed).orEmpty(), + var unlicensedDialogDescription: String? = + context?.getString(R.string.app_unlicensed_description).orEmpty() + ) { + + private var display: Display? = null + + @ColorRes + private var colorPrimary: Int = 0 + + @ColorRes + private var colorPrimaryDark: Int = 0 + private var withLightStatusBar: Boolean = false + + @LayoutRes + private var layoutXML = -1 + private var enableLVL: Boolean = false + private var enableSigningCertificate: Boolean = false + private var enableUnauthorizedAppsCheck: Boolean = false + private var enableStoresCheck: Boolean = false + private var enableEmulatorCheck: Boolean = false + private var enableDeepEmulatorCheck: Boolean = false + private var enableDebugCheck: Boolean = false + private var enableFoldersCheck: Boolean = false + private var enableAPKCheck: Boolean = false + private var saveToSharedPreferences: Boolean = false + private var blockUnauthorized: Boolean = false + private var preferences: SharedPreferences? = null + private var preferenceSaveResult: String? = null + private var preferenceBlockUnauthorized: String? = null + private var licenseBase64: String? = null + private var signatures: Array = arrayOf() + private val installerIDs: MutableList + private val extraApps: ArrayList + + private var allowCallback: AllowCallback? = null + private var doNotAllowCallback: DoNotAllowCallback? = null + private var onErrorCallback: OnErrorCallback? = null + + // LVL + private var libraryLVLChecker: LibraryChecker? = null + + // Dialog + private var dialog: PiracyCheckerDialog? = null + + init { + this.display = Display.DIALOG + this.installerIDs = ArrayList() + this.extraApps = ArrayList() + this.colorPrimary = R.color.colorPrimary + this.colorPrimaryDark = R.color.colorPrimaryDark + } + + constructor(context: Context?) : + this( + context, context?.getString(R.string.app_unlicensed).orEmpty(), + context?.getString(R.string.app_unlicensed_description).orEmpty()) + + constructor(context: Context?, title: String?) : + this( + context, title.orEmpty(), + context?.getString(R.string.app_unlicensed_description).orEmpty()) + + constructor(context: Context?, @StringRes title: Int) : + this( + context, + if (title != 0) context?.getString(title).orEmpty() else "") + + constructor(context: Context?, @StringRes title: Int, @StringRes description: Int) : + this( + context, + if (title != 0) context?.getString(title).orEmpty() else "", + if (description != 0) context?.getString(description).orEmpty() else "") + + fun enableGooglePlayLicensing(licenseKeyBase64: String): PiracyChecker { + this.enableLVL = true + this.licenseBase64 = licenseKeyBase64 + return this + } + + @Deprecated( + "Deprecated in favor of enableSigningCertificates so you can check for multiple signatures", + ReplaceWith("enableSigningCertificates(signature)")) + fun enableSigningCertificate(signature: String): PiracyChecker { + this.enableSigningCertificate = true + this.signatures = arrayOf(signature) + return this + } + + fun enableSigningCertificates(vararg signatures: String): PiracyChecker { + this.enableSigningCertificate = true + this.signatures = arrayOf(*signatures) + return this + } + + fun enableSigningCertificates(signatures: List): PiracyChecker { + this.enableSigningCertificate = true + this.signatures = signatures.toTypedArray() + return this + } + + fun enableInstallerId(vararg installerID: InstallerID): PiracyChecker { + this.installerIDs.addAll(listOf(*installerID)) + return this + } + + fun enableUnauthorizedAppsCheck(): PiracyChecker { + this.enableUnauthorizedAppsCheck = true + return this + } + + fun blockIfUnauthorizedAppUninstalled( + preferences: SharedPreferences, + preferenceName: String + ): PiracyChecker { + this.blockUnauthorized = true + this.preferenceBlockUnauthorized = preferenceName + saveToSharedPreferences(preferences) + return this + } + + fun blockIfUnauthorizedAppUninstalled( + preferencesName: String, + preferenceName: String + ): PiracyChecker { + this.blockUnauthorized = true + this.preferenceBlockUnauthorized = preferenceName + saveToSharedPreferences(preferencesName) + return this + } + + fun enableStoresCheck(): PiracyChecker { + this.enableStoresCheck = true + return this + } + + fun enableDebugCheck(): PiracyChecker { + this.enableDebugCheck = true + return this + } + + fun enableAPKCheck(): PiracyChecker { + this.enableAPKCheck = true + return this + } + + fun enableEmulatorCheck(deepCheck: Boolean): PiracyChecker { + this.enableEmulatorCheck = true + this.enableDeepEmulatorCheck = deepCheck + return this + } + + fun enableFoldersCheck(): PiracyChecker { + this.enableFoldersCheck = true + return this + } + + fun addAppToCheck(vararg apps: PirateApp): PiracyChecker { + this.extraApps.addAll(Arrays.asList(*apps)) + return this + } + + fun addAppToCheck(app: PirateApp): PiracyChecker { + this.extraApps.add(app) + return this + } + + fun saveResultToSharedPreferences( + preferences: SharedPreferences, + preferenceName: String + ): PiracyChecker { + this.saveToSharedPreferences = true + this.preferenceSaveResult = preferenceName + saveToSharedPreferences(preferences) + return this + } + + fun saveResultToSharedPreferences( + preferencesName: String, + preferenceName: String + ): PiracyChecker { + this.saveToSharedPreferences = true + this.preferenceSaveResult = preferenceName + saveToSharedPreferences(preferencesName) + return this + } + + private fun saveToSharedPreferences(preferences: SharedPreferences?) { + if (preferences != null) { + this.preferences = preferences + } else { + try { + this.preferences = (context as? Activity)?.getPreferences(Context.MODE_PRIVATE) + } catch (e: Exception) { + this.preferences = + context?.getSharedPreferences(LIBRARY_PREFERENCES_NAME, Context.MODE_PRIVATE) + } + } + } + + private fun saveToSharedPreferences(preferencesName: String?) { + if (preferencesName != null) { + this.preferences = context?.getSharedPreferences(preferencesName, Context.MODE_PRIVATE) + } else { + try { + this.preferences = (context as? Activity)?.getPreferences(Context.MODE_PRIVATE) + } catch (e: Exception) { + this.preferences = + context?.getSharedPreferences(LIBRARY_PREFERENCES_NAME, Context.MODE_PRIVATE) + } + } + } + + fun display(display: Display): PiracyChecker { + this.display = display + return this + } + + fun withActivityColors( + @ColorRes colorPrimary: Int, + @ColorRes colorPrimaryDark: Int, + withLightStatusBar: Boolean + ): PiracyChecker { + this.colorPrimary = colorPrimary + this.colorPrimaryDark = colorPrimaryDark + this.withLightStatusBar = withLightStatusBar + return this + } + + fun withActivityLayout(@LayoutRes layout: Int): PiracyChecker { + this.layoutXML = layout + return this + } + + fun allowCallback(allowCallback: AllowCallback): PiracyChecker { + this.allowCallback = allowCallback + return this + } + + fun doNotAllowCallback(doNotAllowCallback: DoNotAllowCallback): PiracyChecker { + this.doNotAllowCallback = doNotAllowCallback + return this + } + + fun onErrorCallback(errorCallback: OnErrorCallback): PiracyChecker { + this.onErrorCallback = errorCallback + return this + } + + fun callback(callback: PiracyCheckerCallback): PiracyChecker { + this.allowCallback = object : AllowCallback { + override fun allow() { + callback.allow() + } + } + this.doNotAllowCallback = object : DoNotAllowCallback { + override fun doNotAllow(error: PiracyCheckerError, app: PirateApp?) { + callback.doNotAllow(error, app) + } + } + this.onErrorCallback = object : OnErrorCallback { + override fun onError(error: PiracyCheckerError) { + super.onError(error) + callback.onError(error) + } + } + return this + } + + fun destroy() { + dismissDialog() + destroyLVLChecker() + context = null + } + + fun start() { + if (allowCallback == null && doNotAllowCallback == null) { + callback(object : PiracyCheckerCallback() { + override fun allow() {} + + override fun doNotAllow(error: PiracyCheckerError, app: PirateApp?) { + if (context is Activity && (context as Activity).isFinishing) { + return + } + + val dialogContent = when { + app != null -> + context?.getString(R.string.unauthorized_app_found, app.name).orEmpty() + error == PiracyCheckerError.BLOCK_PIRATE_APP -> + context?.getString(R.string.unauthorized_app_blocked).orEmpty() + else -> unlicensedDialogDescription + } + + if (display == Display.DIALOG) { + dismissDialog() + dialog = PiracyCheckerDialog.newInstance( + unlicensedDialogTitle.orEmpty(), dialogContent.orEmpty() + ) + + context?.let { + dialog?.show(it) ?: { + Log.e( + "PiracyChecker", + "Unlicensed dialog was not built properly. Make sure your context is an instance of Activity") + }() + } + } else { + val intent = Intent(context, LicenseActivity::class.java) + .putExtra("content", dialogContent) + .putExtra("colorPrimary", colorPrimary) + .putExtra("colorPrimaryDark", colorPrimaryDark) + .putExtra("withLightStatusBar", withLightStatusBar) + .putExtra("layoutXML", layoutXML) + context?.startActivity(intent) + (context as? Activity)?.finish() + destroy() + } + } + }) + } + verify() + } + + private fun verify() { + // Library will check first the non-LVL methods since LVL is asynchronous and could take + // some seconds to give a result + if (!verifySigningCertificate()) { + doNotAllowCallback?.doNotAllow(PiracyCheckerError.SIGNATURE_NOT_VALID, null) + } else if (!verifyInstallerId()) { + doNotAllowCallback?.doNotAllow(PiracyCheckerError.INVALID_INSTALLER_ID, null) + } else if (!verifyUnauthorizedApp()) { + doNotAllowCallback?.doNotAllow(PiracyCheckerError.BLOCK_PIRATE_APP, null) + } else { + if (enableLVL) { + val deviceId = + Settings.Secure.getString(context?.contentResolver, Settings.Secure.ANDROID_ID) + destroyLVLChecker() + libraryLVLChecker = + LibraryChecker( + context, + ServerManagedPolicy( + context, + AESObfuscator( + SaltUtils.getSalt(context), context?.packageName, deviceId) + ), + licenseBase64) + libraryLVLChecker?.checkAccess(object : LibraryCheckerCallback { + override fun allow(reason: Int) { + doExtraVerification(true) + } + + override fun dontAllow(reason: Int) { + doExtraVerification(false) + } + + override fun applicationError(errorCode: Int) { + onErrorCallback?.onError( + PiracyCheckerError.getCheckerErrorFromCode(errorCode)) + } + }) + } else { + doExtraVerification(true) + } + } + } + + private fun verifySigningCertificate(): Boolean { + return !enableSigningCertificate || (context?.verifySigningCertificates(signatures) == true) + } + + private fun verifyInstallerId(): Boolean { + return installerIDs.isEmpty() || (context?.verifyInstallerId(installerIDs) == true) + } + + private fun verifyUnauthorizedApp(): Boolean { + return !blockUnauthorized || + !(preferences?.getBoolean(preferenceBlockUnauthorized, false) ?: false) + } + + private fun doExtraVerification( + possibleSuccess: Boolean + ) { + val app = context?.getPirateApp( + enableUnauthorizedAppsCheck, enableStoresCheck, enableFoldersCheck, enableAPKCheck, + extraApps) + if (possibleSuccess) { + if (enableDebugCheck && (context?.isDebug() == true)) { + if (saveToSharedPreferences) + preferences?.edit()?.putBoolean(preferenceSaveResult, false)?.apply() + doNotAllowCallback?.doNotAllow(PiracyCheckerError.USING_DEBUG_APP, null) + } else if (enableEmulatorCheck && isInEmulator(enableDeepEmulatorCheck)) { + if (saveToSharedPreferences) + preferences?.edit()?.putBoolean(preferenceSaveResult, false)?.apply() + doNotAllowCallback?.doNotAllow(PiracyCheckerError.USING_APP_IN_EMULATOR, null) + } else if (app != null) { + if (saveToSharedPreferences) + preferences?.edit()?.putBoolean(preferenceSaveResult, false)?.apply() + if (blockUnauthorized && app.type == AppType.PIRATE) + preferences?.edit()?.putBoolean(preferenceBlockUnauthorized, true)?.apply() + doNotAllowCallback?.doNotAllow( + if (app.type == AppType.STORE) + PiracyCheckerError.THIRD_PARTY_STORE_INSTALLED + else + PiracyCheckerError.PIRATE_APP_INSTALLED, app) + } else { + if (saveToSharedPreferences) + preferences?.edit()?.putBoolean(preferenceSaveResult, true)?.apply() + allowCallback?.allow() + } + } else { + if (app != null) { + if (saveToSharedPreferences) + preferences?.edit()?.putBoolean(preferenceSaveResult, false)?.apply() + if (blockUnauthorized && app.type == AppType.PIRATE) + preferences?.edit()?.putBoolean(preferenceBlockUnauthorized, true)?.apply() + doNotAllowCallback?.doNotAllow( + if (app.type == AppType.STORE) + PiracyCheckerError.THIRD_PARTY_STORE_INSTALLED + else + PiracyCheckerError.PIRATE_APP_INSTALLED, app) + } else { + if (saveToSharedPreferences) + preferences?.edit()?.putBoolean(preferenceSaveResult, false)?.apply() + doNotAllowCallback?.doNotAllow(PiracyCheckerError.NOT_LICENSED, null) + } + } + } + + private fun dismissDialog() { + dialog?.dismiss() + dialog = null + } + + private fun destroyLVLChecker() { + libraryLVLChecker?.finishAllChecks() + libraryLVLChecker?.onDestroy() + libraryLVLChecker = null + } + + companion object { + private const val LIBRARY_PREFERENCES_NAME = "license_check" + } +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/PiracyCheckerDialog.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/PiracyCheckerDialog.kt new file mode 100644 index 0000000..2162ed8 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/PiracyCheckerDialog.kt @@ -0,0 +1,36 @@ +package com.frogobox.sdkutil.piracychecker + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.DialogFragment +import com.frogobox.sdkutil.piracychecker.utils.buildUnlicensedDialog + +class PiracyCheckerDialog : DialogFragment() { + + fun show(context: Context) { + (context as? AppCompatActivity)?.let { + pcDialog?.show(it.supportFragmentManager, "[LICENSE_DIALOG]") + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + super.onCreateDialog(savedInstanceState) + isCancelable = false + return activity?.buildUnlicensedDialog(title.orEmpty(), content.orEmpty())!! + } + + companion object { + private var pcDialog: PiracyCheckerDialog? = null + private var title: String? = null + private var content: String? = null + + fun newInstance(dialogTitle: String, dialogContent: String): PiracyCheckerDialog? { + pcDialog = PiracyCheckerDialog() + title = dialogTitle + content = dialogContent + return pcDialog + } + } +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/activities/ActivityUtils.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/activities/ActivityUtils.kt new file mode 100644 index 0000000..d557705 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/activities/ActivityUtils.kt @@ -0,0 +1,36 @@ +package com.frogobox.sdkutil.piracychecker.activities + +import android.content.Context +import android.os.Build +import android.view.View + +internal fun Context.getAppName(): String { + var name: String = try { + (packageManager?.getApplicationLabel(applicationInfo) ?: "").toString() + } catch (e: Exception) { + "" + } + if (name.isNotBlank() && name.isNotEmpty()) return name + + val stringRes = applicationInfo?.labelRes ?: 0 + name = if (stringRes == 0) { + applicationInfo?.nonLocalizedLabel?.toString() ?: "" + } else { + try { + getString(stringRes) + } catch (e: Exception) { + "" + } + } + return name +} + +internal fun View.setupLightStatusBar(enable: Boolean) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + var flags = systemUiVisibility + flags = + if (enable) flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + else flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + systemUiVisibility = flags + } +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/activities/LicenseActivity.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/activities/LicenseActivity.kt new file mode 100644 index 0000000..d69f655 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/activities/LicenseActivity.kt @@ -0,0 +1,78 @@ +package com.frogobox.sdkutil.piracychecker.activities + +import android.annotation.SuppressLint +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import com.frogobox.sdkutil.piracychecker.R + +class LicenseActivity : AppCompatActivity() { + private var description: String? = null + + @ColorRes + private var colorPrimary: Int = 0 + + @ColorRes + private var colorPrimaryDark: Int = 0 + private var withLightStatusBar: Boolean = false + + @LayoutRes + private var layoutXML: Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_license) + getIntentData() + setActivityStyle() + setActivityData() + } + + private fun getIntentData() { + description = intent?.getStringExtra("content").orEmpty() + colorPrimary = intent?.getIntExtra( + "colorPrimary", ContextCompat.getColor(this, R.color.colorPrimary)) + ?: ContextCompat.getColor(this, R.color.colorPrimary) + colorPrimaryDark = intent?.getIntExtra( + "colorPrimaryDark", ContextCompat.getColor(this, R.color.colorPrimaryDark)) + ?: ContextCompat.getColor(this, R.color.colorPrimaryDark) + withLightStatusBar = intent?.getBooleanExtra("withLightStatusBar", false) ?: false + layoutXML = intent?.getIntExtra("layoutXML", -1) ?: -1 + } + + private fun setActivityStyle() { + val toolbar = findViewById(R.id.toolbar) as? Toolbar + toolbar?.setBackgroundColor(ContextCompat.getColor(this, colorPrimary)) + setSupportActionBar(toolbar) + supportActionBar?.title = getAppName() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.statusBarColor = ContextCompat.getColor(this, colorPrimaryDark) + } + + window.decorView.setupLightStatusBar(withLightStatusBar) + } + + @SuppressLint("InflateParams") + private fun setActivityData() { + val frameLayout = findViewById(R.id.mainContainer) + + val factory = LayoutInflater.from(this) + val inflateView: View? + if (layoutXML == -1) { + inflateView = factory.inflate(R.layout.activity_license_default, null) + val activityDescription = + inflateView.findViewById(R.id.piracy_checker_description) + activityDescription?.text = description + } else inflateView = factory.inflate(layoutXML, null) + + inflateView?.let { frameLayout?.addView(it) } + } +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/callbacks/PiracyCheckerCallbacks.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/callbacks/PiracyCheckerCallbacks.kt new file mode 100644 index 0000000..60fea45 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/callbacks/PiracyCheckerCallbacks.kt @@ -0,0 +1,47 @@ +package com.frogobox.sdkutil.piracychecker.callbacks + +import com.frogobox.sdkutil.piracychecker.enums.PiracyCheckerError +import com.frogobox.sdkutil.piracychecker.enums.PirateApp + +interface AllowCallback { + /** + * Called after the app checked as valid and licensed + */ + fun allow() +} + +interface DoNotAllowCallback { + @Deprecated("dontAllow has been deprecated in favor of doNotAllow", ReplaceWith("doNotAllow")) + fun dontAllow(error: PiracyCheckerError, app: PirateApp?) = + doNotAllow(error, app) + + /** + * Called if the app is not valid or the user is using an unlicensed version. Check errors at + * [PiracyCheckerError]. + * + * @param error + * PiracyCheckerError.NOT_LICENSED, PiracyCheckerError.SIGNATURE_NOT_VALID or + * PiracyCheckerError.INVALID_INSTALLER_ID + * @param app + * The [PirateApp] that has been detected on device. Returns null in no app was + * found. + */ + fun doNotAllow(error: PiracyCheckerError, app: PirateApp?) +} + +interface OnErrorCallback { + /** + * Called if an error with the license check occurs. Check errors at [ ]. + * + * @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 + */ + fun onError(error: PiracyCheckerError) {} +} + +abstract class PiracyCheckerCallback : AllowCallback, + DoNotAllowCallback, + OnErrorCallback \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/callbacks/PiracyCheckerCallbacksDSL.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/callbacks/PiracyCheckerCallbacksDSL.kt new file mode 100644 index 0000000..cfd08e6 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/callbacks/PiracyCheckerCallbacksDSL.kt @@ -0,0 +1,14 @@ +package com.frogobox.sdkutil.piracychecker.callbacks + +import com.frogobox.sdkutil.piracychecker.PiracyChecker + +class PiracyCheckerCallbacksDSL internal constructor(private val checker: PiracyChecker) { + fun allow(allowCallback: AllowCallback): PiracyChecker = + checker.allowCallback(allowCallback) + + fun doNotAllow(doNotAllowCallback: DoNotAllowCallback): PiracyChecker = + checker.doNotAllowCallback(doNotAllowCallback) + + fun onError(onErrorCallback: OnErrorCallback): PiracyChecker = + checker.onErrorCallback(onErrorCallback) +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/AppType.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/AppType.kt new file mode 100644 index 0000000..9208270 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/AppType.kt @@ -0,0 +1,3 @@ +package com.frogobox.sdkutil.piracychecker.enums + +enum class AppType { PIRATE, STORE, OTHER } \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/Display.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/Display.kt new file mode 100644 index 0000000..a0b8269 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/Display.kt @@ -0,0 +1,3 @@ +package com.frogobox.sdkutil.piracychecker.enums + +enum class Display { DIALOG, ACTIVITY } \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/InstallerID.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/InstallerID.kt new file mode 100644 index 0000000..d70f0dc --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/InstallerID.kt @@ -0,0 +1,24 @@ +package com.frogobox.sdkutil.piracychecker.enums + +import java.util.ArrayList + +enum class InstallerID(private val text: String) { + GOOGLE_PLAY("com.android.vending|com.google.android.feedback"), + AMAZON_APP_STORE("com.amazon.venezia"), + GALAXY_APPS("com.sec.android.app.samsungapps"), + HUAWEI_APP_GALLERY("com.huawei.appmarket"); + + /* (non-Javadoc) + * @see java.lang.Enum#toString() + */ + override fun toString(): String { + return text + } + + fun toIDs(): List = if (text.contains("|")) { + val split = text.split("\\|".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + ArrayList(listOf(*split)) + } else { + ArrayList(listOf(text)) + } +} diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/PiracyCheckerError.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/PiracyCheckerError.kt new file mode 100644 index 0000000..2269ff8 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/PiracyCheckerError.kt @@ -0,0 +1,41 @@ +package com.frogobox.sdkutil.piracychecker.enums + +enum class PiracyCheckerError(private val text: String) { + 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."), + USING_DEBUG_APP("This is a debug build."), + USING_APP_IN_EMULATOR("This app is being used in an emulator."), + PIRATE_APP_INSTALLED("At least one pirate app has been detected on device."), + BLOCK_PIRATE_APP( + "At least one pirate app has been detected and the app must be reinstalled when all " + + "unauthorized apps are uninstalled."), + THIRD_PARTY_STORE_INSTALLED("At least one third-party store has been detected on device."), + + // 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."); + + /* (non-Javadoc) + * @see java.lang.Enum#toString() + */ + override fun toString(): String = text + + companion object { + fun getCheckerErrorFromCode(errorCode: Int): PiracyCheckerError = when (errorCode) { + 1 -> INVALID_PACKAGE_NAME + 2 -> NON_MATCHING_UID + 3 -> NOT_MARKET_MANAGED + 4 -> CHECK_IN_PROGRESS + 5 -> INVALID_PUBLIC_KEY + 6 -> MISSING_PERMISSION + else -> UNKNOWN + } + } +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/PirateApp.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/PirateApp.kt new file mode 100644 index 0000000..b7dfd51 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/enums/PirateApp.kt @@ -0,0 +1,32 @@ +package com.frogobox.sdkutil.piracychecker.enums + +import android.text.TextUtils + +class PirateApp(name: String, pack: Array, type: AppType = AppType.OTHER) { + var name: String? = null + private set + var type: AppType? = null + private set + private var pack: Array? = null + + init { + this.name = name + this.pack = pack.clone() + this.type = type + } + + @Deprecated("Deprecated in favor of packageName", ReplaceWith("packageName")) + val `package`: String + get() = packageName + + val packageName: String + get() { + val sb = StringBuilder() + pack?.forEach { sb.append(it) } + return sb.toString() + } + + @JvmOverloads + constructor(name: String, appPackage: String, type: AppType = AppType.OTHER) : + this(name, TextUtils.split(appPackage, ""), type) +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/utils/LibraryUtils.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/utils/LibraryUtils.kt new file mode 100644 index 0000000..c264293 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/utils/LibraryUtils.kt @@ -0,0 +1,628 @@ +package com.frogobox.sdkutil.piracychecker.utils + +import android.Manifest +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.Signature +import android.opengl.GLES20 +import android.os.Build +import android.os.Environment +import android.util.Base64 +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityCompat +import com.frogobox.sdkutil.piracychecker.R +import com.frogobox.sdkutil.piracychecker.enums.AppType +import com.frogobox.sdkutil.piracychecker.enums.InstallerID +import com.frogobox.sdkutil.piracychecker.enums.PirateApp +import java.io.File +import java.security.MessageDigest +import java.util.ArrayList + +internal fun Context.buildUnlicensedDialog(title: String, content: String): AlertDialog? { + return (this as? Activity)?.let { + if (isFinishing) return null + AlertDialog.Builder(this) + .setCancelable(false) + .setTitle(title) + .setMessage(content) + .setPositiveButton( + getString(R.string.app_unlicensed_close), + DialogInterface.OnClickListener { _, _ -> + if (isFinishing) + return@OnClickListener + finish() + }) + .create() + } +} + +@Deprecated( + "Deprecated in favor of apkSignatures, which returns all valid signing signatures", + ReplaceWith("apkSignatures")) +val Context.apkSignature: Array + get() = apkSignatures + +val Context.apkSignatures: Array + get() = currentSignatures + +@Suppress("DEPRECATION", "RemoveExplicitTypeArguments") +private val Context.currentSignatures: Array + get() { + val actualSignatures = ArrayList() + val signatures: Array = try { + val packageInfo = + packageManager.getPackageInfo( + packageName, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + PackageManager.GET_SIGNING_CERTIFICATES + else PackageManager.GET_SIGNATURES) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (packageInfo.signingInfo.hasMultipleSigners()) + packageInfo.signingInfo.apkContentsSigners + else packageInfo.signingInfo.signingCertificateHistory + } else packageInfo.signatures + } catch (e: Exception) { + arrayOf() + } + signatures.forEach { signature -> + val messageDigest = MessageDigest.getInstance("SHA") + messageDigest.update(signature.toByteArray()) + try { + actualSignatures.add( + Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT).trim()) + } catch (e: Exception) { + } + } + return actualSignatures.filter { it.isNotEmpty() && it.isNotBlank() }.toTypedArray() + } + +private fun Context.verifySigningCertificate(appSignature: String?): Boolean = + appSignature?.let { appSign -> currentSignatures.any { it == appSign } } ?: false + +internal fun Context.verifySigningCertificates(appSignatures: Array): Boolean { + var validCount = 0 + appSignatures.forEach { if (verifySigningCertificate(it)) validCount += 1 } + return validCount >= appSignatures.size +} + +internal fun Context.verifyInstallerId(installerID: List): Boolean { + val validInstallers = ArrayList() + val installer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + packageManager.getInstallSourceInfo(packageName).installingPackageName + } else { + packageManager.getInstallerPackageName(packageName) + } + for (id in installerID) { + validInstallers.addAll(id.toIDs()) + } + return installer != null && validInstallers.contains(installer) +} + +@Suppress("DEPRECATION") +@SuppressLint("SdCardPath") +internal fun Context.getPirateApp( + lpf: Boolean, + stores: Boolean, + folders: Boolean, + apks: Boolean, + extraApps: ArrayList + ): PirateApp? { + if (!lpf && !stores && extraApps.isEmpty()) return null + + val apps = getApps(extraApps) + var installed = false + var theApp: PirateApp? = null + + try { + val pm = packageManager + val list = pm?.getInstalledApplications(PackageManager.GET_META_DATA) + for (app in apps) { + val checkLPF = lpf && app.type == AppType.PIRATE + val checkStore = stores && app.type == AppType.STORE + val checkOther = app.type == AppType.OTHER + if (checkLPF || checkStore || checkOther) { + installed = list?.any { it.packageName.contains(app.packageName) } ?: false + if (!installed) { + installed = isIntentAvailable(pm.getLaunchIntentForPackage(app.packageName)) + } + } + if (installed) { + theApp = app + break + } + } + } catch (e: Exception) { + } + + if ((folders || apks) && theApp == null) { + if (hasPermissions()) { + var apkExist = false + var foldersExist = false + var containsFolder = false + + for (app in apps) { + val pack = app.packageName + try { + if (apks) { + val file1 = File("/data/app/$pack-1/base.apk") + val file2 = File("/data/app/$pack-2/base.apk") + val file3 = File("/data/app/$pack.apk") + val file4 = File("/data/data/$pack.apk") + apkExist = file1.exists() || file2.exists() || + file3.exists() || file4.exists() + } + if (folders) { + val file5 = File("/data/data/$pack") + val file6 = + File( + "${Environment.getExternalStorageDirectory()}/Android/data/$pack") + foldersExist = file5.exists() || file6.exists() + + val appsContainer = File("/data/app/") + if (appsContainer.exists()) { + for (f in appsContainer.listFiles().orEmpty()) { + if (f.name.startsWith(pack)) + containsFolder = true + } + } + } + } catch (e: Exception) { + } + if (containsFolder || apkExist || foldersExist) { + theApp = app + break + } + } + } + } + + return theApp +} + +/** + * 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. + * + * + * Copyright (C) 2013, Vladislav Gingo Skoumal (http://www.skoumal.net) + */ +@Suppress("DEPRECATION") +internal fun isInEmulator(deepCheck: Boolean = false): Boolean { + var ratingCheckEmulator = 0 + + val product = try { + Build.PRODUCT + } catch (e: Exception) { + "" + } + if (product.containsIgnoreCase("sdk") || product.containsIgnoreCase("Andy") || + product.containsIgnoreCase("ttVM_Hdragon") || product.containsIgnoreCase("google_sdk") || + product.containsIgnoreCase("Droid4X") || product.containsIgnoreCase("nox") || + product.containsIgnoreCase("sdk_x86") || product.containsIgnoreCase("sdk_google") || + product.containsIgnoreCase("vbox86p")) { + ratingCheckEmulator++ + } + + val manufacturer = try { + Build.MANUFACTURER + } catch (e: Exception) { + "" + } + if (manufacturer.equalsIgnoreCase("unknown") || manufacturer.equalsIgnoreCase("Genymotion") || + manufacturer.containsIgnoreCase("Andy") || manufacturer.containsIgnoreCase("MIT") || + manufacturer.containsIgnoreCase("nox") || manufacturer.containsIgnoreCase("TiantianVM")) { + ratingCheckEmulator++ + } + + val brand = try { + Build.BRAND + } catch (e: Exception) { + "" + } + if (brand.equalsIgnoreCase("generic") || brand.equalsIgnoreCase("generic_x86") || + brand.equalsIgnoreCase("TTVM") || brand.containsIgnoreCase("Andy")) { + ratingCheckEmulator++ + } + + val device = try { + Build.DEVICE + } catch (e: Exception) { + "" + } + if (device.containsIgnoreCase("generic") || device.containsIgnoreCase("generic_x86") || + device.containsIgnoreCase("Andy") || device.containsIgnoreCase("ttVM_Hdragon") || + device.containsIgnoreCase("Droid4X") || device.containsIgnoreCase("nox") || + device.containsIgnoreCase("generic_x86_64") || device.containsIgnoreCase("vbox86p")) { + ratingCheckEmulator++ + } + + val model = try { + Build.MODEL + } catch (e: Exception) { + "" + } + if (model.equalsIgnoreCase("sdk") || model.equalsIgnoreCase("google_sdk") || + model.containsIgnoreCase("Droid4X") || model.containsIgnoreCase("TiantianVM") || + model.containsIgnoreCase("Andy") || model.equalsIgnoreCase( + "Android SDK built for x86_64") || + model.equalsIgnoreCase("Android SDK built for x86")) { + ratingCheckEmulator++ + } + + val hardware = try { + Build.HARDWARE + } catch (e: Exception) { + "" + } + if (hardware.equalsIgnoreCase("goldfish") || hardware.equalsIgnoreCase("vbox86") || + hardware.containsIgnoreCase("nox") || hardware.containsIgnoreCase("ttVM_x86")) { + ratingCheckEmulator++ + } + + val fingerprint = try { + Build.FINGERPRINT + } catch (e: Exception) { + "" + } + if (fingerprint.containsIgnoreCase("generic") || + fingerprint.containsIgnoreCase("generic/sdk/generic") || + fingerprint.containsIgnoreCase("generic_x86/sdk_x86/generic_x86") || + fingerprint.containsIgnoreCase("Andy") || fingerprint.containsIgnoreCase("ttVM_Hdragon") || + fingerprint.containsIgnoreCase("generic_x86_64") || + fingerprint.containsIgnoreCase("generic/google_sdk/generic") || + fingerprint.containsIgnoreCase("vbox86p") || + fingerprint.containsIgnoreCase("generic/vbox86p/vbox86p")) { + ratingCheckEmulator++ + } + + if (deepCheck) { + try { + GLES20.glGetString(GLES20.GL_RENDERER)?.let { + if (it.containsIgnoreCase("Bluestacks") || it.containsIgnoreCase("Translator")) + ratingCheckEmulator += 10 + } + } catch (e: Exception) { + } + + try { + val sharedFolder = File( + "${Environment.getExternalStorageDirectory()}${File.separatorChar}windows" + + "${File.separatorChar}BstSharedFolder") + if (sharedFolder.exists()) + ratingCheckEmulator += 10 + } catch (e: Exception) { + } + } + + return ratingCheckEmulator > 3 +} + +internal fun Context.isDebug(): Boolean = + applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 + +private fun getApps(extraApps: ArrayList): ArrayList { + val apps = ArrayList() + apps.add( + PirateApp( + "LuckyPatcher", + arrayOf( + "c", "o", "m", ".", "c", "h", "e", "l", "p", "u", "s", ".", "l", "a", "c", "k", "y", + "p", "a", "t", "c", "h"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "LuckyPatcher", + arrayOf( + "c", "o", "m", ".", "d", "i", "m", "o", "n", "v", "i", "d", "e", "o", ".", "l", "u", + "c", "k", "y", "p", "a", "t", "c", "h", "e", "r"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "LuckyPatcher", + arrayOf("c", "o", "m", ".", "f", "o", "r", "p", "d", "a", ".", "l", "p"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "LuckyPatcher", + arrayOf( + "c", "o", "m", ".", "a", "n", "d", "r", "o", "i", "d", ".", "v", "e", "n", "d", "i", + "n", "g", ".", "b", "i", "l", "l", "i", "n", "g", ".", "I", "n", "A", "p", "p", "B", + "i", "l", "l", "i", "n", "g", "S", "e", "r", "v", "i", "c", "e"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "LuckyPatcher", + arrayOf( + "c", "o", "m", ".", "a", "n", "d", "r", "o", "i", "d", ".", "v", "e", "n", "d", "i", + "n", "g", ".", "b", "i", "l", "l", "i", "n", "g", ".", "I", "n", "A", "p", "p", "B", + "i", "l", "l", "i", "n", "g", "S", "o", "r", "v", "i", "c", "e"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "LuckyPatcher", + arrayOf( + "c", "o", "m", ".", "a", "n", "d", "r", "o", "i", "d", ".", "v", "e", "n", "d", "i", + "n", "c"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "UretPatcher", + arrayOf( + "u", "r", "e", "t", ".", "j", "a", "s", "i", "2", "1", "6", "9", ".", "p", "a", "t", + "c", "h", "e", "r"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "UretPatcher", + arrayOf( + "z", "o", "n", "e", ".", "j", "a", "s", "i", "2", "1", "6", "9", ".", "u", "r", "e", + "t", "p", "a", "t", "c", "h", "e", "r"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "ActionLauncherPatcher", + arrayOf("p", ".", "j", "a", "s", "i", "2", "1", "6", "9", ".", "a", "l", "3"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Freedom", + arrayOf( + "c", "c", ".", "m", "a", "d", "k", "i", "t", "e", ".", "f", "r", "e", "e", "d", "o", + "m"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Freedom", + arrayOf( + "c", "c", ".", "c", "z", ".", "m", "a", "d", "k", "i", "t", "e", ".", "f", "r", "e", + "e", "d", "o", "m"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "CreeHack", + arrayOf( + "o", "r", "g", ".", "c", "r", "e", "e", "p", "l", "a", "y", "s", ".", "h", "a", "c", + "k"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "HappyMod", + arrayOf("c", "o", "m", ".", "h", "a", "p", "p", "y", "m", "o", "d", ".", "a", "p", "k"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Game Hacker", + arrayOf( + "o", "r", "g", ".", "s", "b", "t", "o", "o", "l", "s", ".", "g", "a", "m", "e", "h", + "a", "c", "k"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Game Killer Cheats", + arrayOf( + "c", "o", "m", ".", "z", "u", "n", "e", ".", "g", "a", "m", "e", "k", "i", "l", "l", + "e", "r"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "AGK - App Killer", + arrayOf("c", "o", "m", ".", "a", "a", "g", ".", "k", "i", "l", "l", "e", "r"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Game Killer", + arrayOf( + "c", "o", "m", ".", "k", "i", "l", "l", "e", "r", "a", "p", "p", ".", "g", "a", "m", + "e", "k", "i", "l", "l", "e", "r"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Game Killer", arrayOf("c", "n", ".", "l", "m", ".", "s", "q"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Game CheatIng Hacker", + arrayOf( + "n", "e", "t", ".", "s", "c", "h", "w", "a", "r", "z", "i", "s", ".", "g", "a", "m", + "e", "_", "c", "i", "h"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Game Hacker", + arrayOf( + "c", "o", "m", ".", "b", "a", "s", "e", "a", "p", "p", "f", "u", "l", "l", ".", "f", + "w", "d"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Content Guard Disabler", + arrayOf( + "c", "o", "m", ".", "g", "i", "t", "h", "u", "b", ".", "o", "n", "e", "m", "i", "n", + "u", "s", "o", "n", "e", ".", "d", "i", "s", "a", "b", "l", "e", "c", "o", "n", "t", + "e", "n", "t", "g", "u", "a", "r", "d"), + AppType.PIRATE) + ) + + apps.add( + PirateApp( + "Content Guard Disabler", + arrayOf( + "c", "o", "m", ".", "o", "n", "e", "m", "i", "n", "u", "s", "o", "n", "e", ".", "d", + "i", "s", "a", "b", "l", "e", "c", "o", "n", "t", "e", "n", "t", "g", "u", "a", "r", + "d"), + AppType.PIRATE) + ) + apps.add( + PirateApp( + "Aptoide", arrayOf("c", "m", ".", "a", "p", "t", "o", "i", "d", "e", ".", "p", "t"), + AppType.STORE) + ) + apps.add( + PirateApp( + "BlackMart", + arrayOf( + "o", "r", "g", ".", "b", "l", "a", "c", "k", "m", "a", "r", "t", ".", "m", "a", "r", + "k", "e", "t"), + AppType.STORE) + ) + apps.add( + PirateApp( + "BlackMart", + arrayOf( + "c", "o", "m", ".", "b", "l", "a", "c", "k", "m", "a", "r", "t", "a", "l", "p", "h", + "a"), + AppType.STORE) + ) + apps.add( + PirateApp( + "Mobogenie", + arrayOf("c", "o", "m", ".", "m", "o", "b", "o", "g", "e", "n", "i", "e"), + AppType.STORE) + ) + apps.add( + PirateApp( + "1Mobile", + arrayOf( + "m", "e", ".", "o", "n", "e", "m", "o", "b", "i", "l", "e", ".", "a", "n", "d", "r", + "o", "i", "d"), + AppType.STORE) + ) + apps.add( + PirateApp( + "GetApk", arrayOf( + "c", "o", "m", ".", "r", "e", "p", "o", "d", "r", "o", "i", "d", ".", "a", "p", "p"), + AppType.STORE) + ) + apps.add( + PirateApp( + "GetJar", + arrayOf( + "c", "o", "m", ".", "g", "e", "t", "j", "a", "r", ".", "r", "e", "w", "a", "r", "d", + "s"), + AppType.STORE) + ) + apps.add( + PirateApp( + "SlideMe", + arrayOf( + "c", "o", "m", ".", "s", "l", "i", "d", "e", "m", "e", ".", "s", "a", "m", ".", "m", + "a", "n", "a", "g", "e", "r"), + AppType.STORE) + ) + apps.add( + PirateApp( + "ACMarket", + arrayOf("n", "e", "t", ".", "a", "p", "p", "c", "a", "k", "e"), + AppType.STORE) + ) + apps.add( + PirateApp( + "ACMarket", + arrayOf("a", "c", ".", "m", "a", "r", "k", "e", "t", ".", "s", "t", "o", "r", "e"), + AppType.STORE) + ) + apps.add( + PirateApp( + "AppCake", + arrayOf("c", "o", "m", ".", "a", "p", "p", "c", "a", "k", "e"), + AppType.STORE) + ) + apps.add( + PirateApp( + "Z Market", + arrayOf("c", "o", "m", ".", "z", "m", "a", "p", "p"), + AppType.STORE) + ) + apps.add( + PirateApp( + "Modded Play Store", + arrayOf( + "c", "o", "m", ".", "d", "v", ".", "m", "a", "r", "k", "e", "t", "m", "o", "d", ".", + "i", "n", "s", "t", "a", "l", "l", "e", "r"), + AppType.STORE) + ) + apps.add( + PirateApp( + "Mobilism Market", + arrayOf( + "o", "r", "g", ".", "m", "o", "b", "i", "l", "i", "s", "m", ".", "a", "n", "d", "r", + "o", "i", "d"), + AppType.STORE) + ) + apps.add( + PirateApp( + "All-in-one Downloader", arrayOf( + "c", "o", "m", ".", "a", "l", "l", "i", "n", "o", "n", "e", ".", "f", "r", "e", "e"), + AppType.STORE) + ) + apps.addAll(extraApps) + return ArrayList(apps.distinctBy { it.packageName }) +} + +private fun Context.isIntentAvailable(intent: Intent?): Boolean { + intent ?: return false + return try { + packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + .orEmpty().isNotEmpty() + } catch (e: Exception) { + false + } +} + +private fun Context.hasPermissions(): Boolean { + return try { + Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN || + !shouldAskPermission(Manifest.permission.READ_EXTERNAL_STORAGE) || + !ActivityCompat.shouldShowRequestPermissionRationale( + this as Activity, Manifest.permission.READ_EXTERNAL_STORAGE) + } catch (e: Exception) { + false + } +} + +private fun shouldAskPermission(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + +private fun Context.shouldAskPermission(permission: String): Boolean { + if (shouldAskPermission()) { + val permissionResult = ActivityCompat.checkSelfPermission(this, permission) + return permissionResult != PackageManager.PERMISSION_GRANTED + } + return false +} + +private fun String.equalsIgnoreCase(other: String) = this.equals(other, true) +private fun String.containsIgnoreCase(other: String) = this.contains(other, true) \ No newline at end of file diff --git a/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/utils/SaltUtils.kt b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/utils/SaltUtils.kt new file mode 100644 index 0000000..5a6d119 --- /dev/null +++ b/core-sdk-android-util/src/main/java/com/frogobox/sdkutil/piracychecker/utils/SaltUtils.kt @@ -0,0 +1,65 @@ +package com.frogobox.sdkutil.piracychecker.utils + +import android.content.Context +import androidx.preference.PreferenceManager +import java.util.Random + +/** + * Credits to Aidan Follestad (afollestad) + */ +internal object SaltUtils { + private const val KEY_SALT = "salty-salt" + private var mSalt: ByteArray? = null + + private val saltString: String + get() { + val sb = StringBuilder() + mSalt?.let { + for (i in it.indices) { + if (i > 0) sb.append(" ") + sb.append(it[i].toString()) + } + } + return sb.toString() + } + + private fun generateSalt(context: Context?) { + mSalt = ByteArray(20) + val randomGenerator = Random() + mSalt?.let { + for (i in 0..19) { + it[i] = (randomGenerator.nextInt(600) - 300).toByte() + } + } + context ?: return + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(KEY_SALT, saltString).apply() + } + + private fun bytesFromString(string: String): ByteArray { + val split = string.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val data = ByteArray(split.size) + for (i in split.indices) { + data[i] = java.lang.Byte.parseByte(split[i]) + } + return data + } + + fun getSalt(context: Context?): ByteArray? { + if (mSalt == null) { + mSalt = context?.let { + try { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + if (prefs.contains(KEY_SALT)) { + val saltFromPrefs = prefs.getString(KEY_SALT, null) + saltFromPrefs?.let { bytesFromString(it) } + } else null + } catch (e: Exception) { + null + } + } + if (mSalt == null) generateSalt(context) + } + return mSalt + } +} \ No newline at end of file diff --git a/core-sdk-android-util/src/main/res/layout/activity_license.xml b/core-sdk-android-util/src/main/res/layout/activity_license.xml new file mode 100644 index 0000000..6b15444 --- /dev/null +++ b/core-sdk-android-util/src/main/res/layout/activity_license.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/core-sdk-android-util/src/main/res/layout/activity_license_default.xml b/core-sdk-android-util/src/main/res/layout/activity_license_default.xml new file mode 100644 index 0000000..5f51093 --- /dev/null +++ b/core-sdk-android-util/src/main/res/layout/activity_license_default.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/core-sdk-android-util/src/main/res/values-es/piracychecker_strings.xml b/core-sdk-android-util/src/main/res/values-es/piracychecker_strings.xml new file mode 100644 index 0000000..d1e3a56 --- /dev/null +++ b/core-sdk-android-util/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/core-sdk-android-util/src/main/res/values-es/strings.xml b/core-sdk-android-util/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..a50bf88 --- /dev/null +++ b/core-sdk-android-util/src/main/res/values-es/strings.xml @@ -0,0 +1,6 @@ + + + Licencia no válida + Ésta app no dispone de una licencia válida. Por favor, descárgala de una fuente segura. + Cerrar + diff --git a/core-sdk-android-util/src/main/res/values-sk/strings.xml b/core-sdk-android-util/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000..079d11e --- /dev/null +++ b/core-sdk-android-util/src/main/res/values-sk/strings.xml @@ -0,0 +1,6 @@ + + + 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/core-sdk-android-util/src/main/res/values/colors.xml b/core-sdk-android-util/src/main/res/values/colors.xml new file mode 100644 index 0000000..cb09b5e --- /dev/null +++ b/core-sdk-android-util/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + \ No newline at end of file diff --git a/core-sdk-android-util/src/main/res/values/piracychecker_strings.xml b/core-sdk-android-util/src/main/res/values/piracychecker_strings.xml new file mode 100644 index 0000000..a52f8fd --- /dev/null +++ b/core-sdk-android-util/src/main/res/values/piracychecker_strings.xml @@ -0,0 +1,12 @@ + + + + Javier Santos + https://github.com/javiersantos + PiracyChecker + PiracyChecker prevents your app from being pirated / cracked using Google Play Licensing (LVL), APK signature protection and more. + https://github.com/javiersantos/PiracyChecker + apache_2_0 + true + https://github.com/javiersantos/PiracyChecker + \ No newline at end of file diff --git a/core-sdk-android-util/src/main/res/values/strings.xml b/core-sdk-android-util/src/main/res/values/strings.xml new file mode 100644 index 0000000..83d1e90 --- /dev/null +++ b/core-sdk-android-util/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + This app is not licensed + This application is not licensed nor valid. Please download the app from a trusted source. + Close + License is invalid because we have found you have %1$s app installed on your device. + License is invalid because you have been using an unauthorized app such as Lucky Patcher. Please reinstall the app when the unauthorized app is completely uninstalled. + \ No newline at end of file diff --git a/core-sdk-android-util/src/main/res/values/styles.xml b/core-sdk-android-util/src/main/res/values/styles.xml new file mode 100644 index 0000000..a6cfc9f --- /dev/null +++ b/core-sdk-android-util/src/main/res/values/styles.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/core-sdk-android/build.gradle.kts b/core-sdk-android/build.gradle.kts index 4433d40..435485f 100644 --- a/core-sdk-android/build.gradle.kts +++ b/core-sdk-android/build.gradle.kts @@ -52,27 +52,12 @@ android { dependencies { api(project(DependencyGradle.FROGO_PATH_CORE_SDK)) - - api(Androidx.appCompat) - - api(Androidx.activityKtx) - api(Androidx.fragmentKtx) - - api(Androidx.constraintLayout) - api(Androidx.viewPager2) - - api(Androidx.Core.ktx) - - api(Androidx.Lifecycle.runtimeKtx) - api(Androidx.Lifecycle.viewmodelKtx) - api(Androidx.Lifecycle.livedataKtx) + api(project(DependencyGradle.FROGO_PATH_SDK_UTIL)) api(Androidx.Room.ktx) api(Androidx.Room.runtime) api(Androidx.Room.rxJava3) - api(Google.material) - api(Reactivex.rxAndroid3) api(Koin.android) @@ -82,7 +67,6 @@ dependencies { api(GitHub.chucker) api(GitHub.glide) - api(GitHub.piracyChecker) api(GitHub.customActivityOnCrash) ksp(Androidx.Lifecycle.compiler) diff --git a/core-sdk-android/src/main/java/com/frogobox/sdk/delegate/piracy/PiracyDelegatesImpl.kt b/core-sdk-android/src/main/java/com/frogobox/sdk/delegate/piracy/PiracyDelegatesImpl.kt index 9ea3ede..329152b 100644 --- a/core-sdk-android/src/main/java/com/frogobox/sdk/delegate/piracy/PiracyDelegatesImpl.kt +++ b/core-sdk-android/src/main/java/com/frogobox/sdk/delegate/piracy/PiracyDelegatesImpl.kt @@ -9,12 +9,12 @@ import com.frogobox.sdk.delegate.piracy.util.PiracyMessage import com.frogobox.sdk.ext.getInstallerId import com.frogobox.sdk.ext.showLogD import com.frogobox.sdk.util.FrogoFunc -import com.github.javiersantos.piracychecker.callback -import com.github.javiersantos.piracychecker.doNotAllow -import com.github.javiersantos.piracychecker.enums.Display -import com.github.javiersantos.piracychecker.enums.InstallerID -import com.github.javiersantos.piracychecker.piracyChecker -import com.github.javiersantos.piracychecker.utils.apkSignatures +import com.frogobox.sdkutil.piracychecker.callback +import com.frogobox.sdkutil.piracychecker.doNotAllow +import com.frogobox.sdkutil.piracychecker.enums.Display +import com.frogobox.sdkutil.piracychecker.enums.InstallerID +import com.frogobox.sdkutil.piracychecker.piracyChecker +import com.frogobox.sdkutil.piracychecker.utils.apkSignatures import java.io.BufferedReader import java.io.File import java.io.InputStreamReader diff --git a/gradle.properties b/gradle.properties index 7d4aba6..8b98624 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,6 +19,6 @@ android.useAndroidX=true android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false android.nonFinalResIds=false +android.defaults.buildfeatures.buildconfig=true diff --git a/settings.gradle.kts b/settings.gradle.kts index 12c5529..77afaf3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,5 +20,6 @@ rootProject.name = "FrogoSDK" include( ":app", ":core-sdk", - ":core-sdk-android" + ":core-sdk-android", + ":core-sdk-android-util" ) \ No newline at end of file From 972693e5d829bf3f4bf053db38cfa135d52c4031 Mon Sep 17 00:00:00 2001 From: amirisback Date: Wed, 21 Jun 2023 17:29:10 +0700 Subject: [PATCH 2/5] add: update piracy checker activity --- app/src/main/AndroidManifest.xml | 19 +++++++++++++-- .../com/frogobox/appsdk/main/MainActivity.kt | 23 +++++++++++++++---- app/src/main/res/layout/activity_main.xml | 14 +++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d939c72..c385fb0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,42 +16,57 @@ android:supportsRtl="true" android:theme="@style/ThemeSdk" tools:targetApi="tiramisu"> + + + + + + + + + + + + android:exported="false" + android:theme="@style/AppTheme.NoActionBar"/> + + android:exported="false" + android:theme="@style/AppTheme.NoActionBar"/> + () { } btnWebviewFrogobox.setOnClickListener { - FrogoWebViewActivity.startActivityExt(this@MainActivity, + FrogoWebViewActivity.startActivityExt( + this@MainActivity, "https://frogobox.github.io", - "Frogobox") + "Frogobox" + ) } btnWebviewAmirisback.setOnClickListener { - FrogoWebViewActivity.startActivityExt(this@MainActivity, + FrogoWebViewActivity.startActivityExt( + this@MainActivity, "https://amirisback.github.io", - "Faisal Amir") + "Faisal Amir" + ) } btnAboutUs.setOnClickListener { startActivityExt() } + btnPiracyKotlin.setOnClickListener { + startActivityExt() + } + + btnPiracyMain.setOnClickListener { + startActivityExt() + } + } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0cbd0d2..ace80fd 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -51,6 +51,20 @@ android:layout_marginTop="16dp" android:text="Error Crash" /> +