├── sample
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── ru
│ │ │ └── lazard
│ │ │ └── sample
│ │ │ └── MainActivity.java
│ └── test
│ │ └── java
│ │ └── ru
│ │ └── lazard
│ │ └── sample
│ │ └── ExampleUnitTest.java
├── proguard-rules.pro
└── build.gradle
├── tamperingprotection
├── .gitignore
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── ru
│ │ │ └── lazard
│ │ │ └── tamperingprotection
│ │ │ └── TamperingProtection.java
│ └── test
│ │ └── java
│ │ └── ru
│ │ └── lazard
│ │ └── tamperingprotection
│ │ └── ExampleUnitTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── .idea
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── vcs.xml
├── modules.xml
├── runConfigurations.xml
├── compiler.xml
├── gradle.xml
└── misc.xml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/tamperingprotection/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':tamperingprotection', ':sample'
2 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
12 |
17 |
22 |
27 |
4 |
5 | This Library check is application tampered or not.
6 |
7 | TamperingProtection check:
8 | 1) CRC code of classes.dex - protection from code modification.
9 | 2) application signature - protection from resign you app.
10 | 3) installer store - app must be inbstalled only from store (not by hand).
11 | 4) package name - sometimes malefactor change package name and sells your application as its.
12 | 5) debug mode - production version of app mustn't run in debug mode.
13 | 6) run on emulator - user mustn't run app on emulator.
14 |
15 | You can choose not all of this protection types. Most usefull is "application signature" and "package name".
16 |
17 | How get Signature code:
18 | Use method TamperingProtection.getSignatures(context). This method return fingerprint of current signature.
19 | If app signed by debug keystore then method return debug fingerprint
20 | (if signed by release keystore then return release fingerprint).
21 | Also you can get signature by command line on PC. For get MD5 fingerprint from command line use:
22 | keytool -list -v -keystore <YOU_PATH_TO_KEYSTORE> -alias <YOU_ALIAS> -storepass <YOU_STOREPASS> -keypass <YOU_KEYPASS>
23 |
24 | For get MD5 fingerprint for debug keystore:
25 | keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
26 |
27 | Use only MD5 fingerprint. They looks like: "CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32".
28 |
29 | How get CRC code:
30 | Use method `TamperingProtection.getDexCRC(context)` for get CRC code of classes.dex.
31 | Note: don't keep CRC codes hardcoded in java classes! Keep it in resources (strings.xml), or in JNI code, or WebServer.
32 | CRC code of .dex modified each time when you modify java code.
33 |
34 | ## How to use
35 | Simple usage:
36 | ```java
37 | TamperingProtection protection = new TamperingProtection(context);
38 | protection.setAcceptedPackageNames("ru.lazard.sample"); // your package name
39 | protection.setAcceptedSignatures("CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32"); // MD5 fingerprint
40 |
41 | protection.validateAll();// <- bool is valid or tampered.
42 | ```
43 |
44 |
45 | Max protection varian:
46 | ```java
47 | // Keep dexCrc in resources (strings.xml) or in JNI code. Don't hardcode it in java classes, because it's changes checksum.
48 | long dexCrc = Long.parseLong(this.getResources().getString(R.string.dexCrc));
49 |
50 | TamperingProtection protection = new TamperingProtection(context);
51 | protection.setAcceptedDexCrcs(dexCrc);
52 | protection.setAcceptedStores(TamperingProtection.GOOGLE_PLAY_STORE_PACKAGE); // apps installed only from google play
53 | protection.setAcceptedPackageNames("ru.lazard.sample.Lite_Version","ru.lazard.sample.Pro_Version"); // lite and pro package names
54 | protection.setAcceptedSignatures("CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32"); // only release md5 fingerprint
55 | protection.setAcceptStartOnEmulator(false); // not allowed for emulators
56 | protection.setAcceptStartInDebugMode(false); // not allowed run in debug mode
57 |
58 | protection.validateAllOrThrowException(); // detailed fail information in Exception.
59 | ```
60 |
61 | ## How to install (Gradle)
62 | To get a Git project into your build:
63 |
64 | **Step 1.** Add the JitPack repository to your build file
65 | Add it in your root build.gradle at the end of repositories:
66 |
67 | ```gradle
68 | allprojects {
69 | repositories {
70 | ...
71 | maven { url "https://jitpack.io" }
72 | }
73 | }
74 | ```
75 | **Step 2.** Add the dependency
76 | ```gradle
77 | dependencies {
78 | compile 'com.github.tepikin:AndroidTamperingProtection:0.11'
79 | }
80 | ```
81 | ---
82 | **PS** or just copy file [TamperingProtection.java](https://github.com/tepikin/AndroidTamperingProtection/blob/master/tamperingprotection/src/main/java/ru/lazard/tamperingprotection/TamperingProtection.java) to you project. :)
83 |
--------------------------------------------------------------------------------
/sample/src/main/java/ru/lazard/sample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package ru.lazard.sample;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.view.View;
6 | import android.widget.TextView;
7 |
8 | import ru.lazard.tamperingprotection.TamperingProtection;
9 |
10 | public class MainActivity extends AppCompatActivity implements View.OnClickListener {
11 |
12 | private TextView textView;
13 | private View simpleValidationButton;
14 | private View detailedValidationButton;
15 | private View maxProtectionButton;
16 |
17 |
18 | @Override
19 | protected void onCreate(Bundle savedInstanceState) {
20 | super.onCreate(savedInstanceState);
21 | setContentView(R.layout.activity_main);
22 | textView = (TextView) findViewById(R.id.textView);
23 | simpleValidationButton = findViewById(R.id.simpleValidation);
24 | detailedValidationButton = findViewById(R.id.detailedValidation);
25 | maxProtectionButton = findViewById(R.id.maxProtection);
26 |
27 | simpleValidationButton.setOnClickListener(this);
28 | detailedValidationButton.setOnClickListener(this);
29 | maxProtectionButton.setOnClickListener(this);
30 |
31 | detailedValidation();
32 | }
33 |
34 | private void showText(String message) {
35 | textView.setText(message);
36 | }
37 |
38 |
39 | @Override
40 | public void onClick(View view) {
41 | if (simpleValidationButton == view) {
42 | simpleValidation();
43 | }
44 | if (detailedValidationButton == view) {
45 | detailedValidation();
46 | }
47 | if (maxProtectionButton == view){
48 | maxProtectionExample();
49 | }
50 | }
51 |
52 | private void maxProtectionExample() {
53 | long dexCrc = Long.parseLong(this.getResources().getString(R.string.dexCrc)); // Keep dexCrc in resources (strings.xml) or in JNI code. Not hardcode in java classes.
54 |
55 | TamperingProtection protection = new TamperingProtection(this);
56 | protection.setAcceptedDexCrcs(dexCrc);
57 | protection.setAcceptedStores(TamperingProtection.GOOGLE_PLAY_STORE_PACKAGE);
58 | protection.setAcceptedPackageNames("ru.lazard.sample","ru.lazard.sample.Lite_Version","ru.lazard.sample.Pro_Version");
59 | protection.setAcceptedSignatures("CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32","AC:aC:aB:a3:aC:88:A9:66:aB:0D:C9:a8:aB:A6:aF:a2");
60 | protection.setAcceptStartOnEmulator(false);
61 | protection.setAcceptStartInDebugMode(false);
62 |
63 | try {
64 | protection.validateAllOrThrowException();
65 | showText("Valid");
66 | } catch (TamperingProtection.ValidationException e) {
67 | e.printStackTrace();
68 | showText("FAILED "+ e.getMessage());
69 | }
70 | }
71 |
72 | private void detailedValidation() {
73 | TamperingProtection protection = new TamperingProtection(this);
74 | protection.setAcceptedDexCrcs(); // don't validate classes.dex CRC code.
75 | protection.setAcceptedStores(); // allow all stores
76 | protection.setAcceptedPackageNames("ru.lazard.sample");
77 | protection.setAcceptedSignatures("CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32");
78 | protection.setAcceptStartOnEmulator(true);
79 | protection.setAcceptStartInDebugMode(true);
80 |
81 | try {
82 | protection.validateAllOrThrowException();
83 | showText("Valid");
84 | } catch (TamperingProtection.ValidationException e) {
85 | e.printStackTrace();
86 | showText("FAILED "+e.getMessage());
87 | }
88 | }
89 |
90 | private void simpleValidation() {
91 | TamperingProtection protection = new TamperingProtection(this);
92 | protection.setAcceptedDexCrcs(); // don't validate classes.dex CRC code.
93 | protection.setAcceptedStores(); // allow all stores
94 | protection.setAcceptedPackageNames("ru.lazard.sample");
95 | protection.setAcceptedSignatures("CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32");
96 | protection.setAcceptStartOnEmulator(true);
97 | protection.setAcceptStartInDebugMode(true);
98 | boolean isValid = protection.validateAll();
99 |
100 | showText(isValid ? "Valid" : "Tampered");
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/tamperingprotection/src/main/java/ru/lazard/tamperingprotection/TamperingProtection.java:
--------------------------------------------------------------------------------
1 | package ru.lazard.tamperingprotection;
2 |
3 | import android.content.Context;
4 | import android.content.pm.ApplicationInfo;
5 | import android.content.pm.PackageInfo;
6 | import android.content.pm.PackageManager;
7 | import android.content.pm.Signature;
8 | import android.os.Build;
9 | import android.text.TextUtils;
10 | import android.util.Log;
11 |
12 | import androidx.annotation.NonNull;
13 |
14 |
15 | import com.layapp.collages.BuildConfig;
16 |
17 | import java.io.IOException;
18 | import java.security.MessageDigest;
19 | import java.security.NoSuchAlgorithmException;
20 | import java.util.Arrays;
21 | import java.util.Enumeration;
22 | import java.util.List;
23 | import java.util.zip.ZipEntry;
24 | import java.util.zip.ZipFile;
25 |
26 | import kotlin.jvm.functions.Function1;
27 |
28 | /**
29 | * Class for check is application tampered or not.
TamperingProtection check:
30 | * 1) CRC code of classes.dex - protection from code modification.
31 | * 2) application signature - protection from resign you app.
32 | * 3) installer store - app must be inbstalled only from store (not by hand).
33 | * 4) package name - sometimes malefactor change package name and sells your application as its.
34 | * 5) debug mode - production version of app mustn't run in debug mode.
35 | * 6) run on emulator - user musn't run app on emulator.
36 | *
37 | *
48 | * protection.validateAll();
38 | * Simple usage:
39 | *
40 | * TamperingProtection protection = new TamperingProtection(this);
50 | *
41 | * protection.setAcceptedDexCrcs(); // don't validate classes.dex CRC code.
42 | * protection.setAcceptedStores(); // install from any where
43 | * protection.setAcceptedPackageNames("ru.lazard.sample"); // your package name
44 | * protection.setAcceptedSignatures("CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32"); // MD5 fingerprint
45 | * protection.setAcceptStartOnEmulator(true);// allow run on emulator
46 | * protection.setAcceptStartInDebugMode(true);// allow run in debug mode
47 | *
49 | *
51 | * Created by Egor on 08.11.2016.
52 | */
53 |
54 | public class TamperingProtection {
55 |
56 | /**
57 | * Package name of Google play installer.
58 | */
59 | public static final String GOOGLE_PLAY_STORE_PACKAGE = "com.android.vending";
60 | /**
61 | * Package name of Amazon app store installer.
62 | */
63 | public static final String AMAZON_APP_STORE_PACKAGE = "com.amazon.venezia";
64 | /**
65 | * Package name of Samsung app store installer.
66 | */
67 | public static final String SAMSUNG_APP_STORE_PACKAGE = "com.sec.android.app.samsungapps";
68 |
69 | private final Context context;
70 | private List
Note: CRC code of .arsc modified each time when you modify resources.
85 | *
86 | * @param context
87 | * @return - CRC code of resources.arsc file in apk.
88 | * @throws IOException
89 | */
90 | @NonNull
91 | public static long getResCRC(@NonNull Context context) throws IOException {
92 | ZipFile zf = new ZipFile(context.getPackageCodePath());
93 | long crc = 0;
94 | ZipEntry ze2 = zf.getEntry("resources.arsc");
95 | if (ze2 != null) {
96 | crc+=ze2.getCrc();
97 | }
98 | Log.e("Crc", "RES's summ = " + ze2.getCrc());
99 | return crc;
100 | }
101 |
102 |
103 | public static long getTotalCRC(@NonNull Context context) throws IOException {
104 | ZipFile zf = new ZipFile(context.getPackageCodePath());
105 | long crc = 0;
106 | Enumeration extends ZipEntry> entries = zf.entries();
107 | while(entries.hasMoreElements()){
108 | ZipEntry zipEntry = entries.nextElement();
109 | crc+=zipEntry.getCrc();
110 | }
111 | Log.e("Crc", "Total summ = " +crc);
112 | return crc;
113 | }
114 |
115 |
116 | /**
117 | * Get CRC code of classes.dex file.
Note: CRC code of .dex modified each time when you modify java code.
118 | *
119 | * @param context
120 | * @return - CRC code of classes.dex file in apk.
121 | * @throws IOException
122 | */
123 | @NonNull
124 | public static long getDexCRC(@NonNull Context context) throws IOException {
125 | ZipFile zf = new ZipFile(context.getPackageCodePath());
126 | long crc = 0;
127 | for (int i = 1; i < 1000; i++) {
128 | String index = ""+i;
129 | if (i==1){
130 | index="";
131 | }
132 | String name = "classes" + index + ".dex";
133 | ZipEntry ze = zf.getEntry(name);
134 | if (ze !=null){
135 | crc+=ze.getCrc();
136 | }else{
137 | Log.e("Crc","DEX's summ = "+crc);
138 | return crc;
139 | }
140 | }
141 | Log.e("Crc","DEX's summ = "+crc);
142 | return crc;
143 | }
144 |
145 |
146 | /**
147 | * Get Md5 fingerprint of you app. Method return fingerprint of current signature.
148 | * If app signed by debug keystore then method return debug fingerprint
149 | * (if signed by release keystore then return release fingerprint).
150 | * For get MD5 fingerprint from command line:
151 | * keytool -list -v -keystore <YOU_PATH_TO_KEYSTORE> -alias <YOU_ALIAS> -storepass <YOU_STOREPASS> -keypass <YOU_KEYPASS>
152 | *
153 | * For get MD5 fingerprint for debug keystore:
154 | * keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
155 | *
156 | * Use only MD5 fingerprint. They looks like: "CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32".
157 | *
158 | * @param context
159 | */
160 | @NonNull
161 | public static String[] getSignatures(@NonNull Context context) throws PackageManager.NameNotFoundException, NoSuchAlgorithmException {
162 | PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
163 |
164 | if (packageInfo.signatures == null || packageInfo.signatures.length <= 0) {
165 | return new String[]{};
166 | }
167 |
168 | String[] md5Signatures = new String[packageInfo.signatures.length];
169 |
170 | for (int i = 0; i < packageInfo.signatures.length; i++) {
171 | Signature signature = packageInfo.signatures[i];
172 | if (signature == null) continue;
173 |
174 | MessageDigest md = MessageDigest.getInstance("MD5");
175 | md.update(signature.toByteArray());
176 | byte[] digits = md.digest();
177 |
178 | char[] hexArray = "0123456789ABCDEF".toCharArray();
179 | String md5String = "";
180 | for (byte digit : digits) {
181 | int pos = digit & 0xFF;
182 | md5String += "" + hexArray[pos >> 4] + hexArray[pos & 0x0f] + ":";
183 | }
184 | if (md5String.length() > 0) {
185 | md5String = md5String.substring(0, md5String.length() - 1);
186 | }
187 |
188 | md5Signatures[i] = md5String;
189 | }
190 |
191 |
192 | return md5Signatures;
193 | }
194 |
195 | /**
196 | * Check is current device is emulator.
197 | *
198 | * @return
199 | */
200 | public static boolean isEmulator() {
201 | // received from this project: https://github.com/gingo/android-emulator-detector
202 | int rating = 0;
203 | if (Build.PRODUCT.equals("sdk") ||
204 | Build.PRODUCT.equals("google_sdk") ||
205 | Build.PRODUCT.equals("sdk_x86") ||
206 | Build.PRODUCT.equals("vbox86p")) {
207 | rating++;
208 | }
209 | if (Build.MANUFACTURER.equals("unknown") ||
210 | Build.MANUFACTURER.equals("Genymotion")) {
211 | rating++;
212 | }
213 | if (Build.BRAND.equals("generic") ||
214 | Build.BRAND.equals("generic_x86")) {
215 | rating++;
216 | }
217 | if (Build.DEVICE.equals("generic") ||
218 | Build.DEVICE.equals("generic_x86") ||
219 | Build.DEVICE.equals("vbox86p")) {
220 | rating++;
221 | }
222 | if (Build.MODEL.equals("sdk") ||
223 | Build.MODEL.equals("google_sdk") ||
224 | Build.MODEL.equals("Android SDK built for x86")) {
225 | rating++;
226 | }
227 | if (Build.HARDWARE.equals("goldfish") ||
228 | Build.HARDWARE.equals("vbox86")) {
229 | rating++;
230 | }
231 | if (Build.FINGERPRINT.contains("generic/sdk/generic") ||
232 | Build.FINGERPRINT.contains("generic_x86/sdk_x86/generic_x86") ||
233 | Build.FINGERPRINT.contains("generic/google_sdk/generic") ||
234 | Build.FINGERPRINT.contains("generic/vbox86p/vbox86p")) {
235 | rating++;
236 | }
237 | return rating > 4;
238 | }
239 |
240 | /**
241 | * Check is running in debug mode.
242 | *
243 | * @param context
244 | * @return
245 | */
246 | public static boolean isDebug(Context context) {
247 | boolean isDebuggable = (0 != (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE));
248 | return isDebuggable;
249 | }
250 |
251 | /**
252 | * Get package name of app Installer. Known installers (public Stores) is Google play, Amazon and Samsung store.
253 | * Their package names are:
{@link #GOOGLE_PLAY_STORE_PACKAGE},
{@link #AMAZON_APP_STORE_PACKAGE},
{@link #SAMSUNG_APP_STORE_PACKAGE}.
254 | *
255 | * @param context - package name of app Installer. If return null then app was installed by user (not by store).
256 | * @return
257 | */
258 | public static String getCurrentStore(Context context) {
259 | return context.getPackageManager().getInstallerPackageName(context.getPackageName());
260 | }
261 |
262 | /**
263 | * Get current app package name.
264 | *
265 | * @param context
266 | * @return - current package name
267 | */
268 | public static String getPackageName(Context context) {
269 | return context.getApplicationContext().getPackageName();
270 | }
271 |
272 | /**
273 | * Stores are allowed to install the application. You must set their package names.
For example for Google Play must be "com.android.vending"
274 | *
275 | * @param stores - Package names of stores.
By default allowed installation from anywhere. For production recommended next stores: Google play, Amazon and Samsung store. Their package names are:
{@link #GOOGLE_PLAY_STORE_PACKAGE},
{@link #AMAZON_APP_STORE_PACKAGE},
{@link #SAMSUNG_APP_STORE_PACKAGE}.
276 | */
277 | public void setAcceptedStores(String... stores) {
278 | this.stores = Arrays.asList(stores);
279 | }
280 |
281 | /**
282 | * Package name of you app (or many package names for Pro and Lite versions).
283 | *
284 | * @param packageNames - List of package names.
285 | */
286 | public void setAcceptedPackageNames(String... packageNames) {
287 | this.packageNames = Arrays.asList(packageNames);
288 | }
289 |
290 | /**
291 | * Md5 fingerprint of you app (or many fingerprints for release and debug keystore).
292 | * For get MD5 fingerprint use command line:
293 | * keytool -list -v -keystore <YOU_PATH_TO_KEYSTORE> -alias <YOU_ALIAS> -storepass <YOU_STOREPASS> -keypass <YOU_KEYPASS>
294 | *
295 | * For get MD5 fingerprint for debug keystore:
296 | * keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
297 | *
298 | *
299 | * @param signatures - list of signatures ( MD5 fingerprint of keystore ). Each looks like: "CC:0C:FB:83:8C:88:A9:66:BB:0D:C9:C8:EB:A6:4F:32"
300 | */
301 | public void setAcceptedSignatures(String... signatures) {
302 | this.signatures = Arrays.asList(signatures);
303 | }
304 |
305 | /**
306 | * Is allow start app on emulator or not.
307 | *
308 | * @param isEmulatorAvailable - by default true
309 | */
310 | public void setAcceptStartOnEmulator(boolean isEmulatorAvailable) {
311 | this.isEmulatorAvailable = isEmulatorAvailable;
312 | }
313 |
314 | /**
315 | * Is allow start app in debug mode or not.
316 | *
317 | * @param isDebugAvailable - by default true
318 | */
319 | public void setAcceptStartInDebugMode(boolean isDebugAvailable) {
320 | this.isDebugAvailable = isDebugAvailable;
321 | }
322 |
323 | /**
324 | * Check Crc (checksum) of classes.dex file in apk. It's protection from code modification.
Note: don't keep CRC codes hardcoded in java classes! Keep it in resources (strings.xml), or in JNI code, or WebServer.
325 | *
326 | * @param crcs - by default empty (no crc check).
327 | */
328 | public void setAcceptedDexCrcs(long... crcs) {
329 | this.dexCrcs = crcs;
330 | }
331 |
332 | /**
333 | * Check is app valid or tampered.
334 | *
335 | * @return - True if valid. False if tampered.
336 | */
337 | public boolean validateAll() {
338 | try {
339 | validateAllOrThrowException();
340 | return true;
341 | } catch (ValidationException exception) {
342 | return false;
343 | }
344 | }
345 |
346 | /**
347 | * Check is app valid or tampered. If validation fail, then method throw
348 | * ValidationException with detail description of fail reason.
349 | *
350 | * @return - nothing if valid. Throw ValidationException if tampered.
351 | */
352 | public void validateAllOrThrowException() throws ValidationException {
353 | validateDebugMode();
354 | validateEmulator();
355 | validatePackage();
356 | validateStore();
357 | validateSignature();
358 | validateDexCRC();
359 |
360 | }
361 |
362 | private void validateDebugMode() throws ValidationException {
363 | if (isDebugAvailable) return; // // validation success (no validation need)
364 |
365 | // check by ApplicationInfo
366 | if (isDebug(context))
367 | throw new ValidationException(ValidationException.ERROR_CODE_DEBUG_MODE, "Run in debug mode checked by ApplicationInfo. Flags=" + context.getApplicationInfo().flags);
368 |
369 | // check by BuildConfig
370 | if (BuildConfig.DEBUG)
371 | throw new ValidationException(ValidationException.ERROR_CODE_DEBUG_MODE, "Run in debug mode checked by BuildConfig.");
372 | }
373 |
374 | private void validateEmulator() throws ValidationException {
375 | if (isEmulatorAvailable) return; // validation success (no validation need)
376 | boolean isEmulator = isEmulator();
377 |
378 |
379 | if (isEmulator)
380 | throw new ValidationException(ValidationException.ERROR_CODE_RUN_ON_EMULATOR, "Device looks like emulator.\n" +
381 | "Build.PRODUCT: " + Build.PRODUCT + "\n" +
382 | "Build.MANUFACTURER: " + Build.MANUFACTURER + "\n" +
383 | "Build.BRAND: " + Build.BRAND + "\n" +
384 | "Build.DEVICE: " + Build.DEVICE + "\n" +
385 | "Build.MODEL: " + Build.MODEL + "\n" +
386 | "Build.HARDWARE: " + Build.HARDWARE + "\n" +
387 | "Build.FINGERPRINT: " + Build.FINGERPRINT);
388 | }
389 |
390 | private void validatePackage() throws ValidationException {
391 | if (packageNames == null || packageNames.size() <= 0)
392 | return;// validation success (no validation need)
393 | String packageName = getPackageName(context);
394 | if (TextUtils.isEmpty(packageName))
395 | throw new ValidationException(ValidationException.ERROR_CODE_PACKAGE_NAME_IS_EMPTY, "Current package name is empty: packageName=\"" + packageName + "\";");
396 | for (String allowedPackageName : packageNames) {
397 | if (packageName.equalsIgnoreCase(allowedPackageName)) return;// validation success
398 | }
399 | throw new ValidationException(ValidationException.ERROR_CODE_PACKAGE_NAME_NOT_VALID, "Not valid package name: CurrentPackageName=\"" + packageName + "\"; validPackageNames=" + packageNames.toString() + ";");
400 | }
401 |
402 | private void validateStore() throws ValidationException {
403 | if (stores == null || stores.size() <= 0) return;// validation success (no validation need)
404 | final String installer = getCurrentStore(context);
405 | if (TextUtils.isEmpty(installer))
406 | throw new ValidationException(ValidationException.ERROR_CODE_STORE_IS_EMPTY, "Current store is empty: store=\"" + installer + "\"; App installed by user (not by store).");
407 | for (String allowedStore : stores) {
408 | if (installer.equalsIgnoreCase(allowedStore)) return;// validation success
409 | }
410 | throw new ValidationException(ValidationException.ERROR_CODE_STORE_NOT_VALID, "Not valid store: CurrentStore=\"" + installer + "\"; validStores=" + stores.toString() + ";");
411 | }
412 |
413 | private void validateDexCRC() throws ValidationException {
414 | if (dexCrcs == null || dexCrcs.length <= 0)
415 | return;// validation success (no validation need)
416 | try {
417 | long crc = getDexCRC(context);
418 | for (long allowedDexCrc : dexCrcs) {
419 | if (allowedDexCrc == crc) return;// validation success
420 | }
421 | throw new ValidationException(ValidationException.ERROR_CODE_CRC_NOT_VALID, "Crc code of .dex not valid. CurrentDexCrc=" + crc + " acceptedDexCrcs=" + Arrays.toString(dexCrcs) + ";");
422 | } catch (IOException e) {
423 | e.printStackTrace();
424 | throw new ValidationException(ValidationException.ERROR_CODE_CRC_UNKNOWN_EXCEPTION, "Exception on .dex CNC validation.", e);
425 | }
426 | }
427 |
428 | private void validateSignature() throws ValidationException {
429 | if (signatures == null || signatures.size() <= 0)
430 | return;// validation success (no validation need)
431 | try {
432 | String[] md5Signatures = getSignatures(context);
433 |
434 | if (md5Signatures == null || md5Signatures.length <= 0) {
435 | throw new ValidationException(ValidationException.ERROR_CODE_SIGNATURE_IS_EMPTY, "No signatures found.");
436 | }
437 | // TODO Maybe multiple signatures is a type of tampering, but im not sure. If you sure then uncomment next rows.
438 | // if (md5Signatures.length != 1) {
439 | // throw new ValidationException(ValidationException.ERROR_CODE_SIGNATURE_MULTIPLE, "Multiple signatures found. Total signatures=" + packageInfo.signatures.length + ";");
440 | // }
441 |
442 | for (String md5Signature : md5Signatures) {
443 | for (String allowedSignature : signatures) {
444 | if (md5Signature.equalsIgnoreCase(allowedSignature))
445 | return;// validation success
446 | }
447 | }
448 | throw new ValidationException(ValidationException.ERROR_CODE_SIGNATURE_NOT_VALID, "Not valid signature: CurrentSignatures=" + md5Signatures + "; validSignatures=" + signatures.toString() + ";");
449 | } catch (PackageManager.NameNotFoundException exception) {
450 | throw new ValidationException(ValidationException.ERROR_CODE_SIGNATURE_UNKNOWN_EXCEPTION, "Exception on signature validation.", exception);
451 | } catch (NoSuchAlgorithmException exception) {
452 | throw new ValidationException(ValidationException.ERROR_CODE_SIGNATURE_UNKNOWN_EXCEPTION, "Exception on signature validation.", exception);
453 | }
454 |
455 | }
456 |
457 | /**
458 | * Exception with detailed description of validation fail reason.
459 | * Look to {@link #getErrorCode} for get fail reason details ( and {@link #getMessage} for get text description).
460 | */
461 | public static final class ValidationException extends Exception {
462 | public static final int ERROR_CODE_UNKNOWN_EXCEPTION = 1;
463 | public static final int ERROR_CODE_DEBUG_MODE = 2;
464 | public static final int ERROR_CODE_RUN_ON_EMULATOR = 3;
465 | public static final int ERROR_CODE_PACKAGE_NAME_IS_EMPTY = 4;
466 | public static final int ERROR_CODE_PACKAGE_NAME_NOT_VALID = 5;
467 | public static final int ERROR_CODE_STORE_IS_EMPTY = 6;
468 | public static final int ERROR_CODE_STORE_NOT_VALID = 7;
469 | public static final int ERROR_CODE_SIGNATURE_IS_EMPTY = 8;
470 | public static final int ERROR_CODE_SIGNATURE_MULTIPLE = 9;
471 | public static final int ERROR_CODE_SIGNATURE_NOT_VALID = 10;
472 | public static final int ERROR_CODE_SIGNATURE_UNKNOWN_EXCEPTION = 11;
473 | public static final int ERROR_CODE_CRC_NOT_VALID = 12;
474 | public static final int ERROR_CODE_CRC_UNKNOWN_EXCEPTION = 13;
475 | private final int code;
476 |
477 | public ValidationException(int code, String message) {
478 | super(message);
479 | this.code = code;
480 | }
481 |
482 | public ValidationException(int code, String message, Throwable cause) {
483 | super(message, cause);
484 | this.code = code;
485 | }
486 |
487 | /**
488 | * Get code of fail reason
489 | *
490 | * @return code of fail reason - one of:
491 | *
{@link #ERROR_CODE_UNKNOWN_EXCEPTION},
492 | *
{@link #ERROR_CODE_DEBUG_MODE},
493 | *
{@link #ERROR_CODE_RUN_ON_EMULATOR},
494 | *
{@link #ERROR_CODE_PACKAGE_NAME_IS_EMPTY},
495 | *
{@link #ERROR_CODE_PACKAGE_NAME_NOT_VALID},
496 | *
{@link #ERROR_CODE_STORE_IS_EMPTY},
497 | *
{@link #ERROR_CODE_STORE_NOT_VALID},
498 | *
{@link #ERROR_CODE_SIGNATURE_IS_EMPTY},
499 | *
{@link #ERROR_CODE_SIGNATURE_MULTIPLE},
500 | *
{@link #ERROR_CODE_SIGNATURE_NOT_VALID}
501 | */
502 | public int getErrorCode() {
503 | return code;
504 | }
505 | }
506 | }
507 |
--------------------------------------------------------------------------------