21 |
22 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "java.configuration.updateBuildConfiguration": "interactive"
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 Henrik Herzig
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # SPIC - Simple Play Integrity Checker
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | SPIC (short for **S**imple **P**lay **I**ntegrity **C**hecker) is a simple Android App that demonstrates the usage of the [PlayIntegrity API](https://developer.android.com/google/play/integrity) as well as the deprecated [SafetyNet Attestation API](https://developer.android.com/training/safetynet/attestation).
13 |
14 | [](https://play.google.com/store/apps/details?id=com.henrikherzig.playintegritychecker)
17 |
18 |
19 | # Usage
20 | The app sends a request to the Play Integrity API or SafetyNet Attestation API verifies the request locally on the Android Device or on a remote Server using the [Server Implementation](https://github.com/herzhenr/spic-server) (URL can be defined in settings) and shows the result of the verdict to the user. The Raw JSON result can also be viewed and copied to the clipboard.
21 |
22 | Before calling the API to do the attestation a nonce has to be generated which can happen on the device locally (only recommended for testing purposes) or on the server implementation (recommended for production). When using the PlayIntegrity API the verdict check can also be toggled between google and server. The only difference is when using the server the response encryption keys are managed locally on the server and the decryption of the verdict takes place here whereas when using the google option they are managed by google directly and the verdict is decrypted there.
23 |
24 | On the settings page a url to reach the Server Implementation can be set. Toggeling between System/Light/Dark mode of the app can be done here as well. The about page contains some more resources about the used APIs as well as the App itself.
25 |
26 | # Disclaimer
27 | If you plan on using the Play Integrity / SafetyNet Attestation API in your own app, you should propably use a encrypted connection between the server and the client. Local checks on the Android Devices shouldn't be implemented either. Ideally you should pair this API with another authentication method. Be warned: This implementation is just a proof of concept!
28 |
29 | # Screenshots
30 |
31 | Play Integrity Request | supports Dark Mode
32 | :-------------------------:|:-------------------------:
33 |  | 
34 |
35 | Settings Page | About Page
36 | :-------------------------:|:-------------------------:
37 |  | 
38 |
39 | # Download
40 |
41 | - [Google Play](https://play.google.com/store/apps/details?id=com.henrikherzig.playintegritychecker)
42 | - [GitHub Releases](https://github.com/herzhenr/spic-android/releases)
43 |
44 | # Build
45 |
46 | - build the app using AndroidStudio or the `gradlew :app:aR` command
47 | - if the server functionality is desired, refer to the documentation of the [Server Implementation](https://github.com/herzhenr/spic-server) to set things like environment variables
48 |
49 | Some sepcific notes to use the SafetyNet and PlayIntegrity API:
50 |
51 | ## SafetyNet:
52 | - Obtain an API Key for the SafetyNet Attestation API from Google follwing the [official documentation](https://developer.android.com/training/safetynet/attestation#obtain-api-key)
53 | - add it to to `local.properties` as `api_key=...`
54 |
55 | ## PlayIntegrity:
56 | - in order to decrypt the integrity verdict locally on the device or the server you have to manage the corresponding decryption and verification keys by yourself. This is only possible if you have a paid Google Developer Account and registered the app bundle in the [Google Play Console](https://play.google.com/console/about/). Navigate to **Release** -> **Setup** -> **AppIntegrity** -> **Response encryption**, click on **Change** and choose **Manage and download my response encryption keys**. Follow the instructions to create a private-public key pair in order to download the encrypted keys.
57 | - add the keys to `local.properties` as `base64_of_encoded_decryption_key=...` and `base64_of_encoded_verification_key=...`
58 |
59 | Final local.properties file should look like this:
60 | ```
61 | sdk.dir = YOUR SDK PATH
62 | api_key = YOUR KEY
63 | base64_of_encoded_decryption_key = YOUR DECRYPTION KEY
64 | base64_of_encoded_verification_key = YOUR VERIFICATION KEY
65 | ```
66 |
67 | # Architecture
68 | The app is being developed with the help of Jetpack Compose and makes use of the material 2 design components of google. The following APIs are used:
69 | - SafetyNet Attestation API
70 | - Play Integrity API
71 | # Credits
72 | Some parts of the app are inspired and contain code of the [YASNAC](https://github.com/RikkaW/YASNAC) app implementation of the SafetyNet Attestation API by RikkaW. Especially the UI of the verdict result are inspired from this app.
73 | Some other parts of the server implementation (google request for playIntegrity decrypt) are inspired by the app [Play Integrity API Checker](https://github.com/1nikolas/play-integrity-checker-app) by 1nikolas.
74 |
75 |
76 | # License
77 | MIT License
78 |
79 | ```
80 | Copyright (c) 2023 Henrik Herzig
81 |
82 | Permission is hereby granted, free of charge, to any person obtaining a copy
83 | of this software and associated documentation files (the "Software"), to deal
84 | in the Software without restriction, including without limitation the rights
85 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
86 | copies of the Software, and to permit persons to whom the Software is
87 | furnished to do so, subject to the following conditions:
88 |
89 | The above copyright notice and this permission notice shall be included in all
90 | copies or substantial portions of the Software.
91 |
92 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
93 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
94 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
95 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
96 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
97 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
98 | SOFTWARE.
99 | ```
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | ./release/*.aab
3 | ./debug/*.aab
4 | *.aab
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'com.mikepenz.aboutlibraries.plugin'
5 | }
6 |
7 | Properties properties = new Properties()
8 | File localPropFile = project.rootProject.file("local.properties")
9 | if (localPropFile.exists()) {
10 | properties.load(localPropFile.newDataInputStream())
11 | }
12 |
13 | android {
14 | compileSdk 33
15 |
16 | defaultConfig {
17 | applicationId "com.henrikherzig.playintegritychecker"
18 | minSdk 26
19 | targetSdk 33
20 | versionCode 7
21 | versionName "1.4.0"
22 |
23 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
24 | vectorDrawables {
25 | useSupportLibrary true
26 | }
27 | }
28 |
29 | buildTypes {
30 | debug {
31 | buildConfigField("String", "api_key", "\"" + properties['api_key'] + "\"")
32 | buildConfigField("String", "base64_of_encoded_decryption_key", "\"" + properties['base64_of_encoded_decryption_key'] + "\"")
33 | buildConfigField("String", "base64_of_encoded_verification_key", "\"" + properties['base64_of_encoded_verification_key'] + "\"")
34 | }
35 | release {
36 | minifyEnabled false
37 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
38 | buildConfigField("String", "api_key", "\"" + properties['api_key'] + "\"")
39 | buildConfigField("String", "base64_of_encoded_decryption_key", "\"" + properties['base64_of_encoded_decryption_key'] + "\"")
40 | buildConfigField("String", "base64_of_encoded_verification_key", "\"" + properties['base64_of_encoded_verification_key'] + "\"")
41 | }
42 | }
43 | compileOptions {
44 | sourceCompatibility JavaVersion.VERSION_1_8
45 | targetCompatibility JavaVersion.VERSION_1_8
46 | }
47 | kotlinOptions {
48 | jvmTarget = '1.8'
49 | }
50 | buildFeatures {
51 | compose true
52 | }
53 | composeOptions {
54 | kotlinCompilerExtensionVersion compose_version
55 | }
56 | packagingOptions {
57 | resources {
58 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
59 | }
60 | }
61 | }
62 |
63 | dependencies {
64 |
65 | implementation 'androidx.core:core-ktx:1.8.0'
66 | implementation "androidx.compose.ui:ui:$compose_version"
67 | implementation "androidx.compose.material:material:$compose_version"
68 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
69 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.0'
70 | implementation 'androidx.activity:activity-compose:1.5.0'
71 | implementation "androidx.compose.material:material-icons-extended:$compose_version"
72 | implementation 'androidx.browser:browser:1.4.0'
73 |
74 | implementation("com.google.android.play:integrity:1.0.1")
75 | implementation("com.google.android.gms:play-services-safetynet:18.0.1")
76 | implementation('com.google.http-client:google-http-client-android:1.42.0') {
77 | // otherwise conflicts, has to be excluded
78 | exclude group: 'org.apache.httpcomponents'
79 | }
80 | implementation("org.bitbucket.b_c:jose4j:0.7.12")
81 | implementation 'com.squareup.okhttp3:okhttp:4.10.0'
82 | implementation "androidx.navigation:navigation-compose:2.5.1"
83 | implementation "com.google.accompanist:accompanist-systemuicontroller:$accompanist_version"
84 | implementation "androidx.datastore:datastore-preferences:1.0.0"
85 | implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
86 |
87 | implementation 'com.google.code.gson:gson:2.9.1'
88 |
89 | implementation "com.google.accompanist:accompanist-pager:0.23.1"
90 |
91 | implementation "com.mikepenz:aboutlibraries-compose:10.5.2"
92 |
93 | testImplementation 'junit:junit:4.13.2'
94 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
95 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
96 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
97 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
98 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
99 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/henrikherzig/playintegritychecker/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.henrikherzig.playintegritychecker", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker
2 |
3 |
4 | import android.content.Context
5 | import android.os.Bundle
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.viewModels
9 | import androidx.datastore.preferences.preferencesDataStore
10 | import com.google.android.gms.common.GoogleApiAvailability
11 | import com.henrikherzig.playintegritychecker.attestation.playintegrity.AttestationCallPlayIntegrity
12 | import com.henrikherzig.playintegritychecker.attestation.safetynet.AttestationCallSafetyNet
13 | import com.henrikherzig.playintegritychecker.ui.navigationbar.BottomNavigationBar
14 | import com.henrikherzig.playintegritychecker.ui.theme.PlayIntegrityCheckerTheme
15 |
16 | // PreferenceDataStore for settings
17 | val Context.dataStore by preferencesDataStore("settings")
18 |
19 | class MainActivity : ComponentActivity() {
20 |
21 | private val viewSafetyNet: AttestationCallSafetyNet by viewModels()
22 | private val viewPlayIntegrity: AttestationCallPlayIntegrity by viewModels()
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | setContent {
27 | PlayIntegrityCheckerTheme() {
28 | // function to be called when safetyNetRequest is made
29 | val onSafetyNetRequest: (String, String, String?) -> Unit =
30 | { nonceGeneration: String, verifyType: String, url: String? ->
31 | viewSafetyNet.safetyNetAttestationRequest(
32 | this@MainActivity,
33 | nonceGeneration,
34 | verifyType,
35 | url
36 | )
37 | }
38 |
39 | // function to be called when playIntegrityRequest is made
40 | val onPlayIntegrityRequest: (String, String, String?) -> Unit =
41 | { nonceGeneration: String, verifyType: String, url: String? ->
42 | viewPlayIntegrity.playIntegrityRequest(
43 | this@MainActivity,
44 | nonceGeneration,
45 | verifyType,
46 | url
47 | )
48 | }
49 |
50 | // get play services Version
51 | val playServiceVersion = try {
52 | val packageInfo = packageManager.getPackageInfo(
53 | GoogleApiAvailability.GOOGLE_PLAY_SERVICES_PACKAGE,
54 | 0
55 | )
56 | packageInfo.versionName
57 | } catch (e: Throwable) {
58 | null
59 | }
60 |
61 | // create MainUI of App
62 | BottomNavigationBar(
63 | viewSafetyNet.safetyNetResult,
64 | onSafetyNetRequest,
65 | viewPlayIntegrity.playIntegrityResult,
66 | onPlayIntegrityRequest,
67 | playServiceVersion
68 | )
69 | }
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/AttestationException.java:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | public final class AttestationException extends Exception {
6 |
7 | @NonNull
8 | private final String message;
9 |
10 | public AttestationException(@NonNull String message) {
11 | super(message);
12 | this.message = message;
13 | }
14 |
15 | public AttestationException(@NonNull String message, @NonNull Throwable cause) {
16 | super(message, cause);
17 | this.message = message;
18 | }
19 |
20 | @NonNull
21 | @Override
22 | public String getMessage() {
23 | return message;
24 | }
25 |
26 | @NonNull
27 | @Override
28 | public synchronized Throwable fillInStackTrace() {
29 | return this;
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/ExecutorRunner.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import java.util.concurrent.Callable
6 | import java.util.concurrent.Executor
7 | import java.util.concurrent.Executors
8 | class ExecutorRunner {
9 | // create a new instance of Executor using any factory methods
10 | private val executor: Executor = Executors.newSingleThreadExecutor()
11 | // handler of UI thread
12 | private val handler = Handler(Looper.getMainLooper())
13 | // callable to communicate the result back to UI
14 | interface Callback {
15 | fun onComplete(result: R)
16 | fun onError(e: Exception?)
17 | }
18 | fun execute(callable: Callable, callback: Callback) {
19 | executor.execute {
20 | val result: R
21 | try {
22 | // execute the callable or any tasks asynchronously
23 | result = callable.call()
24 | handler.post {
25 | // update the result back to UI
26 | callback.onComplete(result)
27 | }
28 | } catch (e: Exception) {
29 | e.printStackTrace()
30 | handler.post { // communicate error or handle
31 | callback.onError(e)
32 | }
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/Requests.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation
2 |
3 | import android.content.ContentValues
4 | import android.util.Log
5 | import okhttp3.OkHttpClient
6 | import okhttp3.Request
7 | import okhttp3.Response
8 | import okhttp3.ResponseBody
9 | import org.json.JSONObject
10 |
11 | /**
12 | * sends generic api call and checks for errors in the response
13 | */
14 | fun getApiCall(apiUrl: String, entrypoint: String, query: String = ""): String {
15 | val client = OkHttpClient()
16 | val request: Request = Request.Builder()
17 | .get()
18 | .url("$apiUrl$entrypoint?$query")
19 | .build()
20 |
21 | val response: Response = client.newCall(request).execute()
22 |
23 | if (!response.isSuccessful) {
24 | val `object` = JSONObject(response.body!!.string())
25 | val messageString = `object`.getString("Error")
26 | Log.d(ContentValues.TAG, "Error response from API Server. Message:\n'$messageString'")
27 | throw AttestationException("Error response from API Server. Message:\n'$messageString'")
28 | // return "Api request error. Code: " + response.code
29 | }
30 | val responseBody: ResponseBody? = response.body
31 |
32 | if (responseBody == null) {
33 | Log.d(ContentValues.TAG, "Error response from API Server (empty response) \n ${response.code}")
34 | throw AttestationException("Error response from API Server (empty response) \n ${response.code}")
35 | }
36 |
37 | return responseBody.string()
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/Statement.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation
2 |
3 | interface Statement {
4 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/playintegrity/AttestationCallPlayIntegrity.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation.playintegrity
2 |
3 | import android.content.ContentValues.TAG
4 | import android.content.Context
5 | import android.os.Handler
6 | import android.os.Looper
7 | import android.util.Base64
8 | import android.util.Log
9 | import androidx.compose.runtime.MutableState
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.lifecycle.ViewModel
12 | import androidx.lifecycle.viewModelScope
13 | import com.google.android.gms.tasks.Task
14 | import com.google.android.play.core.integrity.IntegrityManagerFactory
15 | import com.google.android.play.core.integrity.IntegrityTokenRequest
16 | import com.google.android.play.core.integrity.IntegrityTokenResponse
17 | import com.google.gson.Gson
18 | import com.henrikherzig.playintegritychecker.BuildConfig
19 | import com.henrikherzig.playintegritychecker.attestation.AttestationException
20 | import com.henrikherzig.playintegritychecker.attestation.PlayIntegrityStatement
21 | import com.henrikherzig.playintegritychecker.attestation.getApiCall
22 | import com.henrikherzig.playintegritychecker.ui.ResponseType
23 | import kotlinx.coroutines.CoroutineExceptionHandler
24 | import kotlinx.coroutines.Dispatchers
25 | import kotlinx.coroutines.launch
26 | import org.jose4j.jwe.JsonWebEncryption
27 | import org.jose4j.jws.JsonWebSignature
28 | import org.jose4j.jwx.JsonWebStructure
29 | import org.jose4j.lang.JoseException
30 | import org.json.JSONObject
31 | import java.security.KeyFactory
32 | import java.security.PublicKey
33 | import java.security.spec.X509EncodedKeySpec
34 | import java.util.Base64.getEncoder
35 | import java.util.concurrent.Executor
36 | import java.util.concurrent.Executors
37 | import javax.crypto.SecretKey
38 | import javax.crypto.spec.SecretKeySpec
39 | import kotlin.math.floor
40 |
41 | class AttestationCallPlayIntegrity : ViewModel() {
42 |
43 | val playIntegrityResult: MutableState> =
44 | mutableStateOf(ResponseType.None)
45 |
46 | fun playIntegrityRequest(
47 | context: Context,
48 | nonceGeneration: String,
49 | verifyType: String,
50 | url: String?
51 | ) = viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, _ -> }) {
52 | // set UI to loading (api calls etc. take some time...)
53 | playIntegrityResult.value = ResponseType.Loading
54 |
55 | // Generate nonce
56 | val nonce: String = try {
57 | generateNonce(nonceGeneration, url)
58 | } catch (e: Exception) {
59 | e.printStackTrace()
60 | // print received error message to the UI
61 | playIntegrityResult.value = ResponseType.Failure(e)
62 | return@launch
63 | }
64 |
65 | // Create an instance of a manager.
66 | val integrityManager = IntegrityManagerFactory.create(context)
67 |
68 | // Request the integrity token by providing a nonce.
69 | val integrityTokenResponse: Task =
70 | integrityManager.requestIntegrityToken(
71 | IntegrityTokenRequest.builder()
72 | .setNonce(nonce)
73 | // .setCloudProjectNumber(757430732184) // hardcoded for now
74 | .build()
75 | )
76 |
77 | // do play integrity api call
78 | integrityTokenResponse.addOnSuccessListener { response ->
79 | run {
80 | // get token
81 | val integrityToken: String = response.token()
82 | // println(integrityToken)
83 |
84 | // show received token in UI
85 | // playIntegrityResult.value = ResponseType.SuccessSimple(integrityToken)
86 |
87 | // decode and verify
88 | try {
89 | decodeAndVerify(verifyType, integrityToken, nonceGeneration, url)
90 | } catch (e: JoseException) {
91 | Log.d(TAG, "can't decode Play Integrity response")
92 | e.printStackTrace()
93 | playIntegrityResult.value =
94 | ResponseType.Failure(Throwable("can't decode Play Integrity response"))
95 | return@run
96 | }
97 | }
98 | }.addOnFailureListener { e ->
99 | Log.d(TAG, "API Error, see Android UI for error message")
100 | playIntegrityResult.value = ResponseType.Failure(e)
101 | }
102 | }
103 |
104 | /**
105 | * generates a nonce. Depending on [nonceGeneration] this happens locally on the device or
106 | * remotely on a server with the url [apiServerUrl]
107 | */
108 | private fun generateNonce(nonceGeneration: String, apiServerUrl: String?): String {
109 | val nonce: String = when (nonceGeneration) {
110 | // Generate nonce locally
111 | "local" -> {
112 | getNonceLocal(50)
113 | }
114 | // Receive nonce from the secure server
115 | "server" -> getNonceServer(apiServerUrl)
116 | // unknown nonceGeneration
117 | else -> throw (Throwable(message = "nonceGeneration '$nonceGeneration' is unknown"))
118 | }
119 | return nonce
120 | }
121 |
122 | private fun String.encode(): String {
123 | return Base64.encodeToString(this.toByteArray(charset("UTF-8")), Base64.URL_SAFE)
124 | }
125 |
126 | /**
127 | * generates a nonce locally
128 | */
129 | private fun getNonceLocal(length: Int): String {
130 | var nonce = ""
131 | val allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
132 | for (i in 0 until length) {
133 | nonce += allowed[floor(Math.random() * allowed.length).toInt()].toString()
134 | }
135 | return nonce.encode()
136 | }
137 |
138 | /**
139 | * get nonce form secure server. sends a api request to the server to get the nonce
140 | */
141 | private fun getNonceServer(url: String?): String {
142 | if (url == null || url == "") throw AttestationException("no url for server provided. check server url in settings")
143 | return getApiCall(url, "/api/playintegrity/nonce")
144 | }
145 |
146 | /**
147 | *
148 | */
149 | private fun decodeAndVerify(
150 | verifyType: String,
151 | integrityToken: String,
152 | nonceGeneration: String,
153 | url: String?
154 | ) {
155 | when (verifyType) {
156 | "local" -> {
157 | // decrypt verdict locally and update UI
158 | val decoded: PlayIntegrityStatement = decodeLocally(integrityToken)
159 | playIntegrityResult.value = ResponseType.SuccessPlayIntegrity(decoded)
160 | }
161 | "google", "server" -> {
162 | // Create executor for async execution of API call
163 | val executor: Executor = Executors.newSingleThreadExecutor()
164 | val handler = Handler(Looper.getMainLooper())
165 | executor.execute {
166 | try {
167 | val decoded = checkPlayIntegrityServer(
168 | integrityToken,
169 | verifyType,
170 | nonceGeneration,
171 | url
172 | )
173 | // perform task asynchronously
174 | handler.post {
175 | playIntegrityResult.value =
176 | ResponseType.SuccessPlayIntegrity(decoded)
177 | }
178 | } catch (e: Exception) {
179 | e.printStackTrace()
180 | handler.post {
181 | playIntegrityResult.value = ResponseType.Failure(e)
182 | }
183 | }
184 | }
185 | }
186 | else -> {
187 | playIntegrityResult.value =
188 | ResponseType.Failure(Throwable(message = "verifyType '$verifyType' is unknown"))
189 | }
190 | }
191 | }
192 |
193 | /**
194 | * decodes the integrity verdict locally on the device for test purposes (not recommended for
195 | * production use)
196 | **/
197 | private fun decodeLocally(integrityToken: String): PlayIntegrityStatement {
198 | val base64OfEncodedDecryptionKey = BuildConfig.base64_of_encoded_decryption_key
199 |
200 | // base64OfEncodedDecryptionKey is provided through Play Console.
201 | val decryptionKeyBytes: ByteArray =
202 | Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT)
203 |
204 | // Deserialized encryption (symmetric) key.
205 | val decryptionKey: SecretKey = SecretKeySpec(
206 | decryptionKeyBytes,
207 | /* offset= */ 0,
208 | decryptionKeyBytes.size,
209 | "AES"
210 | )
211 |
212 | val base64OfEncodedVerificationKey =BuildConfig.base64_of_encoded_verification_key
213 | // base64OfEncodedVerificationKey is provided through Play Console.
214 | val encodedVerificationKey: ByteArray =
215 | Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT)
216 |
217 | // Deserialized verification (public) key.
218 | val verificationKey: PublicKey = KeyFactory.getInstance("EC")
219 | .generatePublic(X509EncodedKeySpec(encodedVerificationKey))
220 |
221 |
222 | val jwe: JsonWebEncryption =
223 | JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption
224 | jwe.key = decryptionKey
225 |
226 | // This also decrypts the JWE token.
227 | val compactJws = jwe.payload
228 |
229 | val jws: JsonWebSignature =
230 | JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature
231 | jws.key = verificationKey
232 |
233 | // This also verifies the signature.
234 | val payload: String = jws.payload
235 | println(payload)
236 | return Gson().fromJson(payload, PlayIntegrityStatement::class.java)
237 | }
238 |
239 | private fun checkPlayIntegrityServer(
240 | apiToken: String,
241 | verifyType: String,
242 | nonceGeneration: String,
243 | apiURL: String?
244 | ): PlayIntegrityStatement {
245 | // make api call
246 | if (apiURL == null) throw AttestationException("no url for server provided. check server url in settings")
247 | val response = getApiCall(
248 | apiURL,
249 | "/api/playintegrity/check",
250 | "token=$apiToken&mode=$verifyType&nonce=$nonceGeneration"
251 | )
252 |
253 | val json= JSONObject(response)
254 |
255 | if (json.has("error")) {
256 | Log.d(TAG, "Api request error: " + json.getString("error"))
257 | throw AttestationException("Api request error: " + json.getString("error"))
258 | // return "Api request error: " + json.getString("error")
259 | }
260 |
261 | if (!json.has("deviceIntegrity")) {
262 | Log.d(TAG, "Api request error: Response does not contain deviceIntegrity")
263 | throw AttestationException("Api request error: Response does not contain deviceIntegrity")
264 | // return "Api request error: Response does not contain deviceIntegrity"
265 | }
266 | val jsonString = json.toString()
267 | return Gson().fromJson(jsonString, PlayIntegrityStatement::class.java)
268 | }
269 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/playintegrity/PlayIntegrityStatement.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation
2 |
3 | import com.google.api.client.util.Key
4 |
5 | /**
6 | * A statement returned by the Play Integrity API.
7 | */
8 | class PlayIntegrityStatement : Statement {
9 | /**
10 | * Details about the request
11 | */
12 | @Key
13 | val requestDetails: RequestDetails? = null
14 |
15 | /**
16 | * Details about the integrity of the app
17 | */
18 | @Key
19 | val appIntegrity: AppIntegrity? = null
20 |
21 | /**
22 | * Details about the device integrity
23 | */
24 | @Key
25 | val deviceIntegrity: DeviceIntegrity? = null
26 |
27 | /**
28 | * Details about the account (licensing)
29 | */
30 | @Key
31 | val accountDetails: AccountDetails? = null
32 |
33 | /**
34 | * Details about the environment
35 | */
36 | @Key
37 | val environmentDetails: EnvironmentDetails? = null
38 | }
39 |
40 | class RequestDetails {
41 | /**
42 | * Request package name of the APK that submitted this request.
43 | */
44 | @Key
45 | val requestPackageName: String? = null
46 |
47 | /**
48 | * Timestamp of the request.
49 | * should be checked by server so request cant be collected by attacker and sent back to server
50 | * later on
51 | */
52 | @Key
53 | val timestampMillis: Long? = null
54 |
55 | /**
56 | * Embedded nonce sent as part of the request.
57 | * should be unique for each request and server should check if nonce belongs to this request
58 | */
59 | @Key
60 | val nonce: String? = null
61 | }
62 |
63 | class AppIntegrity {
64 | /**
65 | * general app recognition verdict. If this evaluates to 'UNEVALUATED', no further infos are
66 | * provided in the response, so other fields are empty
67 | */
68 | @Key
69 | val appRecognitionVerdict: String? = null
70 |
71 | /**
72 | * Package name of the APK that submitted this request.
73 | * should be checked by the server if it matches [BuildConfig.APPLICATION_ID]
74 | */
75 | @Key
76 | val packageName: String? = null
77 |
78 | /**
79 | * Digest of certificate of the APK that submitted this request.
80 | * cts profile match should be checked by server
81 | */
82 | @Key
83 | val certificateSha256Digest: ArrayList = ArrayList()
84 |
85 | /**
86 | * versionCode of appIntegrity
87 | */
88 | @Key
89 | val versionCode: String? = null
90 | }
91 |
92 | class DeviceIntegrity {
93 | /**
94 | * deviceRecognitionVerdict. Types of measurements that contributed to this response.
95 | */
96 | @Key
97 | val deviceRecognitionVerdict: ArrayList = ArrayList()
98 |
99 | /**
100 | * Details about the recent device activity
101 | */
102 | @Key
103 | val recentDeviceActivity: RecentDeviceActivity? = null
104 | }
105 |
106 | class RecentDeviceActivity {
107 | /**
108 | * deviceActivityLevel. Tells you how many times your app requested an integrity token
109 | * on a specific device in the last hour.
110 | */
111 | @Key
112 | val deviceActivityLevel: String? = null
113 | }
114 |
115 | class AccountDetails {
116 | /**
117 | * licensing of the app (only through google play distributed apps are licensed here)
118 | */
119 | @Key
120 | val appLicensingVerdict: String? = null
121 | }
122 |
123 | class EnvironmentDetails {
124 | /**
125 | * The environmentDetails field contains a single value, playProtectVerdict, that provides
126 | * information about Google Play Protect on the device.
127 | */
128 | @Key
129 | val playProtectVerdict: String? = null
130 | }
131 |
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/safetynet/AttestationCallSafetyNet.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation.safetynet
2 |
3 | import android.content.ContentValues
4 | import android.content.Context
5 | import android.os.Handler
6 | import android.os.Looper
7 | import android.util.Log
8 | import androidx.compose.runtime.MutableState
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.lifecycle.ViewModel
11 | import androidx.lifecycle.viewModelScope
12 | import com.google.android.gms.common.ConnectionResult
13 | import com.google.android.gms.common.GoogleApiAvailability
14 | import com.google.android.gms.common.api.ApiException
15 | import com.google.android.gms.safetynet.SafetyNet
16 | import com.henrikherzig.playintegritychecker.BuildConfig
17 | import com.henrikherzig.playintegritychecker.attestation.AttestationException
18 | import com.henrikherzig.playintegritychecker.attestation.getApiCall
19 | import com.henrikherzig.playintegritychecker.ui.ResponseType
20 | import kotlinx.coroutines.CoroutineExceptionHandler
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.launch
23 | import org.json.JSONObject
24 | import java.time.Instant
25 | import java.util.*
26 | import java.util.concurrent.Executor
27 | import java.util.concurrent.Executors
28 |
29 |
30 | class AttestationCallSafetyNet : ViewModel() {
31 |
32 | val safetyNetResult: MutableState> =
33 | mutableStateOf(ResponseType.None)
34 |
35 | /**
36 | * API depricated
37 | * calls Googles Safety Net attestation API and process its contents
38 | */
39 | fun safetyNetAttestationRequest(
40 | context: Context,
41 | nonceGeneration: String,
42 | verifyType: String,
43 | url: String?
44 | ) =
45 | viewModelScope.launch(Dispatchers.IO + CoroutineExceptionHandler { _, _ ->
46 |
47 | }) {
48 | // Set UI to loading screen
49 | safetyNetResult.value = ResponseType.Loading
50 |
51 | // Receive the nonce from the secure server.
52 | val nonce: String = try {
53 | generateNonce(nonceGeneration, url)
54 | } catch (e: Exception) {
55 | e.printStackTrace()
56 | // print received error message to the UI
57 | safetyNetResult.value = ResponseType.Failure(e)
58 | return@launch
59 | }
60 |
61 | val apikey = BuildConfig.api_key
62 |
63 | // check if Google Services are available
64 | if (GoogleApiAvailability.getInstance()
65 | .isGooglePlayServicesAvailable(context, 13000000) !=
66 | ConnectionResult.SUCCESS
67 | ) {
68 | // if not, show error message
69 | println("ERROR: Outdated Google Play Services")
70 | safetyNetResult.value = ResponseType.Failure(
71 | AttestationException("GooglePlay Services Error: Outdated Google Play Services")
72 | )
73 | return@launch
74 | // TODO Prompt user to update Google Play Services.
75 | }
76 | // The SafetyNet Attestation API is available after this check
77 |
78 | // actual call to attestation API
79 | SafetyNet.getClient(context.applicationContext).attest(nonce.toByteArray(), apikey)
80 | .addOnSuccessListener {
81 | // Indicates communication with the service was successful.
82 | try {
83 | val token = it.jwsResult
84 | ?: throw AttestationException("No token from SafetyNet Attestation Received")
85 | decodeAndVerify(verifyType, token, nonceGeneration, url, nonce)
86 | } catch (e: Exception) {
87 | Log.w(ContentValues.TAG, "Error in Offline Verify: ", e)
88 | safetyNetResult.value = ResponseType.Failure(e)
89 | }
90 |
91 | }
92 | .addOnFailureListener { e ->
93 | // An error occurred while communicating with the service.
94 | if (e is ApiException) {
95 | // An error with the Google Play services API contains some additional details.
96 | e.statusCode
97 | // You can retrieve the status code using the apiException.statusCode property.
98 | Log.d(
99 | ContentValues.TAG,
100 | "Error with Google Play Services API: " + e.message
101 | )
102 | } else {
103 | // A different, unknown type of error occurred.
104 | Log.d(ContentValues.TAG, "Unknown Error Type: " + e.message)
105 | }
106 | safetyNetResult.value = ResponseType.Failure(e)
107 | }
108 | }
109 |
110 |
111 | /**
112 | * generates a nonce. Depending on [nonceGeneration] this happens locally on the device or
113 | * remotely on a server with the url [apiServerUrl]
114 | */
115 | private fun generateNonce(nonceGeneration: String, apiServerUrl: String?): String {
116 | val nonce: String = when (nonceGeneration) {
117 | // Generate nonce locally
118 | "local" -> {
119 | getNonceLocal()
120 | }
121 | // Receive nonce from the secure server
122 | "server" -> getNonceServer(apiServerUrl)
123 | // unknown nonceGeneration
124 | else -> throw (Throwable(message = "nonceGeneration '$nonceGeneration' is unknown"))
125 | }
126 | return nonce
127 | }
128 |
129 | /**
130 | * generates a nonce locally
131 | */
132 | private fun getNonceLocal(): String {
133 | return UUID.randomUUID().toString()
134 | }
135 |
136 | /**
137 | * get nonce form secure server. sends a api request to the server to get the nonce
138 | */
139 | private fun getNonceServer(url: String?): String {
140 | if (url == null || url == "") throw AttestationException("no url for server provided. check server url in settings")
141 | return getApiCall(url, "/api/safetynet/nonce")
142 | }
143 |
144 | /**
145 | *
146 | */
147 | private fun decodeAndVerify(
148 | verifyType: String,
149 | integrityToken: String,
150 | nonceGeneration: String,
151 | url: String?,
152 | nonce: String
153 | ) {
154 | when (verifyType) {
155 | "local" -> {
156 | // decrypt verdict locally and update UI
157 | val decoded: SafetyNetStatement =
158 | SafetyNetProcess.decode(integrityToken)
159 |
160 | // verify token
161 | // SafetyNetProcess.verify(decoded, nonce)
162 |
163 | safetyNetResult.value = ResponseType.SuccessSafetyNet(decoded)
164 | }
165 | "server" -> {
166 | // Create executor for async execution of API call
167 | val executor: Executor = Executors.newSingleThreadExecutor()
168 | val handler = Handler(Looper.getMainLooper())
169 | executor.execute {
170 | try {
171 | val decoded = checkPlayIntegrityServer(
172 | integrityToken,
173 | verifyType,
174 | nonceGeneration,
175 | url
176 | )
177 | // perform task asynchronously
178 | handler.post {
179 | safetyNetResult.value =
180 | ResponseType.SuccessSafetyNet(decoded)
181 | }
182 | } catch (e: Exception) {
183 | e.printStackTrace()
184 | handler.post {
185 | safetyNetResult.value = ResponseType.Failure(e)
186 | }
187 | }
188 | }
189 | }
190 | else -> {
191 | safetyNetResult.value =
192 | ResponseType.Failure(Throwable(message = "verifyType '$verifyType' is unknown"))
193 | }
194 | }
195 | }
196 |
197 |
198 | private fun checkPlayIntegrityServer(
199 | apiToken: String,
200 | verifyType: String,
201 | nonceGeneration: String,
202 | apiURL: String?
203 | ): SafetyNetStatement {
204 | // make api call
205 | if (apiURL == null) throw AttestationException("no url for server provided. check server url in settings")
206 | val response = getApiCall(
207 | apiURL,
208 | "/api/safetynet/check",
209 | "token=$apiToken&mode=$verifyType&nonce=$nonceGeneration"
210 | )
211 |
212 | val json = JSONObject(response)
213 |
214 | val nonce = json.get("nonce").toString()
215 | val timestampMs = json.getLong("timestampMs")
216 | val apkPackageName = json.get("apkPackageName").toString()
217 | val apkCertificateDigestSha256Array = json.getJSONArray("apkCertificateDigestSha256")
218 | val apkCertificateDigestSha256 =
219 | arrayOfNulls(apkCertificateDigestSha256Array.length())
220 | for (i in 0 until apkCertificateDigestSha256Array.length()) {
221 | apkCertificateDigestSha256[i] = apkCertificateDigestSha256Array.optString(i)
222 | }
223 | val apkDigestSha256 = json.get("apkDigestSha256").toString()
224 | val ctsProfileMatch: Boolean = json.getBoolean("ctsProfileMatch")
225 | val basicIntegrity: Boolean = json.getBoolean("basicIntegrity")
226 | val evaluationType = json.get("evaluationType").toString()
227 |
228 | //val jsonString = json.toString()
229 | return SafetyNetStatement(
230 | nonce,
231 | timestampMs,
232 | apkPackageName,
233 | apkCertificateDigestSha256,
234 | apkDigestSha256,
235 | ctsProfileMatch,
236 | basicIntegrity,
237 | evaluationType
238 | )
239 | //return Gson().fromJson(jsonString, SafetyNetStatement::class.java)
240 | }
241 |
242 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/safetynet/SafetyNetProcess.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation.safetynet
2 |
3 | import androidx.compose.runtime.MutableState
4 | import com.henrikherzig.playintegritychecker.attestation.safetynet.SafetyNetStatement
5 | import com.google.api.client.extensions.android.json.AndroidJsonFactory
6 | import com.google.api.client.json.webtoken.JsonWebSignature
7 | import com.henrikherzig.playintegritychecker.BuildConfig
8 | import com.henrikherzig.playintegritychecker.attestation.AttestationException
9 | import com.henrikherzig.playintegritychecker.ui.ResponseType
10 | import java.io.IOException
11 | import java.security.GeneralSecurityException
12 | import java.security.cert.X509Certificate
13 | import java.time.Duration
14 | import java.time.Instant
15 |
16 | //import org.apache.http.conn.ssl.DefaultHostnameVerifier;
17 | object SafetyNetProcess {
18 | //private static final DefaultHostnameVerifier HOSTNAME_VERIFIER = new DefaultHostnameVerifier();
19 | /**
20 | * Parses and verifies the API response. This should be done on a server in the future because
21 | * local checks can be exploited by APK repackaging but for demonstration purposes this can be
22 | * done locally (Good way to practically show how to bypass local checks)
23 | * @param attestationResponse response of the API call
24 | * @return processed API response
25 | */
26 | fun decode(attestationResponse: String): SafetyNetStatement {
27 | // Parse JSON Web Signature format.
28 | val jws: JsonWebSignature = try {
29 | JsonWebSignature.parser(AndroidJsonFactory.getDefaultInstance())
30 | .setPayloadClass(SafetyNetStatement::class.java).parse(attestationResponse)
31 | } catch (e: IOException) {
32 | System.err.println(
33 | "Failure: " + attestationResponse + " is not valid JWS " +
34 | "format."
35 | )
36 | throw AttestationException(
37 | "Failure: " + attestationResponse + " is not valid JWS " +
38 | "format."
39 | )
40 | }
41 |
42 | // Verify the signature of the JWS and retrieve the signature certificate.
43 | val cert: X509Certificate?
44 | try {
45 | cert = jws.verifySignature()
46 | if (cert == null) {
47 | System.err.println("Failure: Signature verification failed.")
48 | throw AttestationException("Failure: Signature verification failed.")
49 | }
50 | } catch (e: GeneralSecurityException) {
51 | System.err.println(
52 | "Failure: Error during cryptographic verification of the JWS signature."
53 | )
54 | throw AttestationException("Failure: Error during cryptographic verification of the JWS signature.")
55 | }
56 |
57 | // TODO: Verify the hostname of the certificate.
58 | // if (!verifyHostname("attest.android.com", cert)) {
59 | // System.err.println("Failure: Certificate isn't issued for the hostname attest.android" +
60 | // ".com.");
61 | // return null;
62 | // }
63 |
64 | // Extract and use the payload data.
65 | return jws.payload as SafetyNetStatement
66 | }
67 |
68 | // /**
69 | // * Verifies that the certificate matches the specified hostname.
70 | // * Uses the {@link DefaultHostnameVerifier} from the Apache HttpClient library
71 | // * to confirm that the hostname matches the certificate.
72 | // *
73 | // * @param hostname
74 | // * @param leafCert
75 | // * @return
76 | // */
77 | // private static boolean verifyHostname(String hostname, X509Certificate leafCert) {
78 | // try {
79 | // // Check that the hostname matches the certificate. This method throws an exception if
80 | // // the cert could not be verified.
81 | // HOSTNAME_VERIFIER.verify(hostname, leafCert);
82 | // return true;
83 | // } catch (SSLException e) {
84 | // e.printStackTrace();
85 | // }
86 | //
87 | // return false;
88 | // }
89 |
90 | /**
91 | * validates the [decoded] integrity verdict locally on the device for test purposes (not recommended for
92 | * production use). [nonce] is the original nonce which got passes to the safetyNet API call
93 | * throws [AttestationException] if an error occurs
94 | **/
95 | fun verify(decoded: SafetyNetStatement, nonce: String) {
96 | // do more validation
97 | // check if nonce is correct
98 | if (decoded.nonce != nonce) {
99 | throw AttestationException("Wrong nonce received")
100 | }
101 |
102 | // check how long the response took (timeout set to 10 seconds)
103 | val maxDuration = Duration.ofSeconds(10)
104 | val timestamp = Instant.ofEpochMilli(decoded.timestampMs)
105 | val timeout = timestamp.plus(maxDuration)
106 | // TODO somehow, the second check fails on real Galaxy S9 device, requestTimestamp is later than timestamp of response. for now, check has been removed
107 | if (timestamp.isAfter(timeout) /*|| timestamp.isBefore(requestTimestamp)*/) {
108 | throw AttestationException("Timeout")
109 | }
110 |
111 | // check cts profile match and verify ctsProfileMatches
112 | if (decoded.isCtsProfileMatch) {
113 | val ctsProfileMatches = arrayOf(
114 | "rnv+gyOF6I07XyGZzNfPXz5K9zqX5aEzChFdowrzLm0=",
115 | "a+yXX21Qt/feRZckl6bm1awqvzBPGOV9OUZ4HMDOUog=",
116 | "rJfDDAnNl4/Kq2MkpwESX519oXToUgpUnpPmDTQIq2M=",
117 | )
118 | if (ctsProfileMatches.none { it in decoded.apkCertificateDigestSha256 }) {
119 | throw AttestationException("Wrong CertificateDigestSha256")
120 | }
121 |
122 | // verify package name
123 | if (BuildConfig.APPLICATION_ID != decoded.apkPackageName) {
124 | throw AttestationException("Wrong Package name")
125 | }
126 | } else {
127 | print("Warning: no cts profile match")
128 | }
129 | }
130 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/attestation/safetynet/SafetyNetStatement.java:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.attestation.safetynet;
2 |
3 | import androidx.annotation.Nullable;
4 |
5 | import com.google.api.client.json.webtoken.JsonWebSignature;
6 | import com.google.api.client.util.Key;
7 | import com.google.common.io.BaseEncoding;
8 | import com.henrikherzig.playintegritychecker.attestation.Statement;
9 |
10 | import java.nio.charset.StandardCharsets;
11 | import java.util.Arrays;
12 | import java.util.List;
13 |
14 | /**
15 | * A statement returned by the SafetyNet Attestation API.
16 | */
17 | public class SafetyNetStatement extends JsonWebSignature.Payload implements Statement {
18 | /**
19 | * Embedded nonce sent as part of the request.
20 | * should be unique for each request and server should check if nonce belongs to this request
21 | */
22 | @Key
23 | private String nonce;
24 |
25 | /**
26 | * Timestamp of the request.
27 | * should be checked by server so request cant be collected by attacker and sent back to server
28 | * later on
29 | */
30 | @Key
31 | private long timestampMs;
32 |
33 | /**
34 | * Package name of the APK that submitted this request.
35 | * should be checked by the server if it matches [BuildConfig.APPLICATION_ID]
36 | */
37 | @Key
38 | private String apkPackageName;
39 |
40 | /**
41 | * Digest of certificate of the APK that submitted this request.
42 | * cts profile match should be checked by server
43 | */
44 | @Key
45 | private String[] apkCertificateDigestSha256;
46 |
47 | /**
48 | * Digest of the APK that submitted this request.
49 | */
50 | @Key
51 | private String apkDigestSha256;
52 |
53 | /**
54 | * The device passed CTS and matches a known profile.
55 | */
56 | @Key
57 | private boolean ctsProfileMatch;
58 |
59 | /**
60 | * The device has passed a basic integrity test, but the CTS profile could not be verified.
61 | */
62 | @Key
63 | private boolean basicIntegrity;
64 |
65 | /**
66 | * Types of measurements that contributed to this response.
67 | */
68 | @Key
69 | private String evaluationType;
70 |
71 | //TODO parse advice field, and error (and show it in error UI element)
72 |
73 | public SafetyNetStatement() {
74 | }
75 |
76 | public SafetyNetStatement(String nonce, long timestampMs, @Nullable String apkPackageName, String[] apkCertificateDigestSha256, @Nullable String apkDigestSha256, boolean ctsProfileMatch, boolean basicIntegrity, String evaluationType) {
77 | this.nonce = nonce;
78 | this.timestampMs = timestampMs;
79 | this.apkPackageName = apkPackageName;
80 | this.apkCertificateDigestSha256 = apkCertificateDigestSha256;
81 | this.apkDigestSha256 = apkDigestSha256;
82 | this.ctsProfileMatch = ctsProfileMatch;
83 | this.basicIntegrity = basicIntegrity;
84 | this.evaluationType = evaluationType;
85 | }
86 |
87 | public String getNonce() {
88 | return new String(BaseEncoding.base64().decode(nonce), StandardCharsets.UTF_8);
89 | }
90 |
91 | public long getTimestampMs() {
92 | return timestampMs;
93 | }
94 |
95 | public String getApkPackageName() {
96 | return String.valueOf(apkPackageName);
97 | }
98 |
99 | public String getApkDigestSha256() {
100 | return String.valueOf(apkDigestSha256);
101 | }
102 |
103 | public List getApkCertificateDigestSha256() {
104 | return Arrays.asList(apkCertificateDigestSha256);
105 | }
106 |
107 | public boolean isCtsProfileMatch() {
108 | return ctsProfileMatch;
109 | }
110 |
111 | public boolean hasBasicIntegrity() {
112 | return basicIntegrity;
113 | }
114 |
115 | public boolean hasBasicEvaluationType() {
116 | return evaluationType.contains("BASIC");
117 | }
118 |
119 | public boolean hasHardwareBackedEvaluationType() {
120 | return evaluationType.contains("HARDWARE_BACKED");
121 | }
122 |
123 | public String integrityType() {
124 | return evaluationType;
125 | }
126 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/CustomButtonToggleGroup.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.material.*
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.CompositionLocalProvider
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.unit.Dp
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.zIndex
14 |
15 |
16 | /**
17 | * Custom MaterialButtonToggleGroup
18 | */
19 | @OptIn(ExperimentalMaterialApi::class)
20 | @Composable
21 | fun ToggleGroup(
22 | selectedIndex: String,
23 | items: List>,
24 | indexChanged: (String) -> Unit,
25 | height: Dp
26 | ) {
27 | val cornerRadius = 8.dp
28 | Row {
29 | items.forEachIndexed { index, item ->
30 | CompositionLocalProvider(
31 | LocalMinimumTouchTargetEnforcement provides false,
32 | ) {
33 | OutlinedButton(
34 | contentPadding = PaddingValues(horizontal = 0.dp),
35 | modifier = when (index) {
36 | 0 ->
37 | Modifier
38 | .offset(0.dp, 0.dp)
39 | .zIndex(if (selectedIndex == item[0]) 1f else 0f)
40 | .fillMaxWidth().weight(1f)
41 | .height(height)
42 | else ->
43 | Modifier
44 | .offset((-1 * index).dp, 0.dp)
45 | .zIndex(if (selectedIndex == item[0]) 1f else 0f)
46 | .fillMaxWidth().weight(1f)
47 | .height(height)
48 | },
49 | onClick = {
50 | indexChanged(item[0])
51 | //selectedIndex = index
52 | },
53 | shape = when (index) {
54 | // left outer button
55 | 0 -> RoundedCornerShape(
56 | topStart = cornerRadius,
57 | topEnd = 0.dp,
58 | bottomStart = cornerRadius,
59 | bottomEnd = 0.dp
60 | )
61 | // right outer button
62 | items.size - 1 -> RoundedCornerShape(
63 | topStart = 0.dp,
64 | topEnd = cornerRadius,
65 | bottomStart = 0.dp,
66 | bottomEnd = cornerRadius
67 | )
68 | // middle button
69 | else -> RoundedCornerShape(
70 | topStart = 0.dp,
71 | topEnd = 0.dp,
72 | bottomStart = 0.dp,
73 | bottomEnd = 0.dp
74 | )
75 | },
76 | border = BorderStroke(
77 | 1.dp, if (selectedIndex == item[0]) {
78 | MaterialTheme.colors.primary
79 | } else {
80 | Color.DarkGray.copy(alpha = 0.75f)
81 | }
82 | ),
83 | colors = if (selectedIndex == item[0]) {
84 | // selected colors
85 | ButtonDefaults.outlinedButtonColors(
86 | backgroundColor = MaterialTheme.colors.primary.copy(
87 | alpha = 0.1f
88 | ), contentColor = MaterialTheme.colors.primary
89 | )
90 | } else {
91 | // not selected colors
92 | ButtonDefaults.outlinedButtonColors(
93 | backgroundColor = MaterialTheme.colors.surface,
94 | contentColor = MaterialTheme.colors.primary
95 | )
96 | },
97 | ) {
98 | Text(
99 | text = item[1],
100 | color = if (selectedIndex == items[index][0]) {
101 | MaterialTheme.colors.primary
102 | } else {
103 | Color.DarkGray.copy(alpha = 0.9f)
104 | },
105 | )
106 | }
107 | }
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/CustomCardComponents.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.rememberScrollState
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.foundation.text.selection.SelectionContainer
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material.*
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.outlined.*
11 | import androidx.compose.runtime.*
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.graphics.vector.ImageVector
16 | import androidx.compose.ui.platform.LocalClipboardManager
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.text.AnnotatedString
19 | import androidx.compose.ui.text.font.FontFamily
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.tooling.preview.Preview
22 | import androidx.compose.ui.unit.dp
23 | import androidx.compose.ui.unit.sp
24 | import com.henrikherzig.playintegritychecker.R
25 |
26 |
27 | /**
28 | * Custom Card Element
29 | * In this case, it is being used as a wrapper for the API result, loading screen or error screen
30 | */
31 | @Composable
32 | fun CustomCard(content: @Composable () -> Unit) {
33 | Card(
34 | //elevation = 0.dp,
35 | border = ButtonDefaults.outlinedBorder,
36 | modifier = Modifier
37 | .fillMaxWidth(),
38 | content = content,
39 | shape = RoundedCornerShape(10.dp),
40 | //backgroundColor = MaterialTheme.colors.primary
41 | )
42 | }
43 |
44 | /**
45 | * Title Element within the Card
46 | */
47 | @Composable
48 | fun CustomCardTitle(text: String) {
49 | Text(
50 | text = text,
51 | fontWeight = FontWeight.Normal,
52 | fontSize = 20.sp,
53 | modifier = Modifier.padding(bottom = 2.dp),
54 | color = MaterialTheme.colors.primary
55 | )
56 | Spacer(modifier = Modifier.height(6.dp))
57 | }
58 |
59 | /**
60 | * Title Element within the Card (one layer deeper than Title)
61 | */
62 | @Composable
63 | fun CustomCardTitle2(text: String) {
64 | Text(
65 | text = text,
66 | fontWeight = FontWeight.Normal,
67 | fontSize = 16.sp,
68 | modifier = Modifier.padding(bottom = 2.dp),
69 | color = MaterialTheme.colors.primary
70 | )
71 | Spacer(modifier = Modifier.height(3.dp))
72 | }
73 |
74 | /**
75 | * Title Element within the Card (another layer deeper than title2)
76 | */
77 | @Composable
78 | fun CustomCardTitle3(text: String) {
79 | Text(
80 | text = text,
81 | style = MaterialTheme.typography.caption,
82 | color = MaterialTheme.colors.primary
83 | )
84 | }
85 |
86 | /**
87 | * Creates 2 Text Elements on top of each other which resemble the title and the corresponding content
88 | * @param text1 the title of the group
89 | * @param text2 the content of the group
90 | */
91 | @Composable
92 | fun CustomCardGroup(text1: String, text2: String) {
93 | CustomCardTitle3(text1)
94 | //Spacer(modifier = Modifier.height(12.dp))
95 | Text(text = text2)
96 | Spacer(modifier = Modifier.height(12.dp))
97 | }
98 |
99 | /**
100 | * Similar to [CustomCardGroup] but the content is either true or false and is shown with a
101 | * icon next to it
102 | * @param text the title of the group
103 | * @param passed the content of the group (either passed or failed)
104 | */
105 | @Composable
106 | fun CustomCardBool(text: String, passed: Boolean) {
107 | Text(
108 | text = text,
109 | style = MaterialTheme.typography.caption,
110 | color = MaterialTheme.colors.primary
111 | )
112 | Spacer(modifier = Modifier.height(1.dp))
113 |
114 | val result =
115 | if (passed) stringResource(R.string.sn_passed) else stringResource(R.string.sn_failed)
116 | val color = if (passed) MaterialTheme.colors.primary else MaterialTheme.colors.error
117 | val icon = if (passed) Icons.Outlined.CheckCircle else Icons.Outlined.Cancel
118 |
119 | Row(
120 | verticalAlignment = Alignment.CenterVertically
121 | ) {
122 | Icon(
123 | imageVector = icon,
124 | tint = color,
125 | contentDescription = null,
126 | modifier = Modifier.size(16.dp)
127 | )
128 | Spacer(modifier = Modifier.width(4.dp))
129 | Text(
130 | text = result,
131 | color = color
132 | )
133 | }
134 | Spacer(modifier = Modifier.height(12.dp))
135 | }
136 |
137 | /**
138 | * Similar to [CustomCardGroup] but the content is either true, false or unevaluated and is shown
139 | * with a icon next to it
140 | * @param text the title of the group
141 | * @param passed the content of the group (either passed or failed)
142 | */
143 | @Composable
144 | fun CustomCardBoolHorizontal(text: String, passed: Boolean?) {
145 | Row(verticalAlignment = Alignment.CenterVertically) {
146 | val color =
147 | if (passed == true) MaterialTheme.colors.primary else if (passed == false) MaterialTheme.colors.error else Color.Gray
148 | val icon = if (passed == true) Icons.Outlined.CheckCircle else Icons.Outlined.Cancel
149 |
150 | Icon(
151 | imageVector = icon,
152 | tint = color,
153 | contentDescription = null,
154 | modifier = Modifier.size(16.dp)
155 | )
156 | Spacer(modifier = Modifier.width(4.dp))
157 | Text(
158 | text = text,
159 | color = color
160 | )
161 | Spacer(modifier = Modifier.height(12.dp))
162 | }
163 | }
164 |
165 | /**
166 | * Custom button element
167 | */
168 | @Composable
169 | fun CustomButton(onClick: () -> Unit, icon: ImageVector, text: String) {
170 | OutlinedButton(
171 | onClick = onClick,
172 | //shape = MaterialTheme.shapes.small,
173 | shape = RoundedCornerShape(10.dp),
174 | modifier = Modifier
175 | .fillMaxWidth()
176 | .height(40.dp),
177 | //enabled = enabled
178 | ) {
179 | Icon(
180 | icon,
181 | contentDescription = null,
182 | modifier = Modifier.size(ButtonDefaults.IconSize)
183 | )
184 | Spacer(Modifier.size(ButtonDefaults.IconSpacing))
185 | Text(text = text)
186 | }
187 | }
188 |
189 | /**
190 | * shows a alert dialog with a title on top and a ok and copy button in the bottom which has the
191 | * ability to copy the text to the clipboard of the device
192 | * @param titleString Title Text to be displayed
193 | * @param content Text which is being shown within the alert dialog body
194 | * @param opened controls weather the dialog is opened or not
195 | */
196 | @Composable
197 | fun CustomContentAlertDialog(
198 | modifier: Modifier = Modifier,
199 | titleString: String,
200 | content: @Composable (() -> Unit)? = null,
201 | opened: MutableState,
202 | ) {
203 | // val localClipboardManager = LocalClipboardManager.current
204 | CustomAlertDialog(
205 | modifier= modifier,
206 | titleString = titleString,
207 | titleIcon = Icons.Outlined.Help,
208 | opened = opened,
209 | content = content,
210 | )
211 | }
212 |
213 | /**
214 | * shows a alert dialog with a title on top and a ok and copy button in the bottom which has the
215 | * ability to copy the text to the clipboard of the device
216 | * @param content Text which is being shown within the alert dialog body
217 | * @param opened controls weather the dialog is opened or not
218 | */
219 | @Composable
220 | fun CustomCodeAlertDialog(
221 | content: String,
222 | opened: MutableState,
223 | ) {
224 | val localClipboardManager = LocalClipboardManager.current
225 | CustomAlertDialog(
226 | titleString = stringResource(id = R.string.dialog_title),
227 | titleIcon = Icons.Outlined.Code,
228 | opened = opened,
229 | content = {
230 | SelectionContainer {
231 | Text(
232 | text = content,
233 | fontFamily = FontFamily.Monospace,
234 | modifier = Modifier.verticalScroll(rememberScrollState())
235 | )
236 | }
237 | },
238 | dismissButton = {
239 | TextButton(
240 | onClick = { localClipboardManager.setText(AnnotatedString(content)) }) {
241 | Text(stringResource(id = android.R.string.copy))
242 | }
243 | },
244 | )
245 | }
246 |
247 |
248 | /**
249 | * shows a alert dialog with a title on top and a ok button in the bottom
250 | * @param titleString Title Text to be displayed
251 | * @param content Text which is being shown within the alert dialog body
252 | * @param opened controls weather the dialog is opened or not
253 | */
254 | @Composable
255 | fun CustomTextAlertDialog(
256 | titleString: String,
257 | content: String,
258 | opened: MutableState,
259 | ) {
260 | CustomAlertDialog(
261 | titleString = titleString,
262 | titleIcon = Icons.Outlined.Help,
263 | opened = opened,
264 | content = {
265 | SelectionContainer {
266 | Text(
267 | text = content,
268 | modifier = Modifier.verticalScroll(rememberScrollState())
269 | )
270 | }
271 | },
272 | )
273 | }
274 |
275 | /**
276 | * custom alert dialog which is the foundation for other custom alert dialogs
277 | * @param titleString Title Text to be displayed
278 | * @param titleIcon Title icon to be displayed
279 | * @param content Content which is being shown within the alert dialog body
280 | * @param opened controls weather the dialog is opened or not
281 | */
282 | @Composable
283 | fun CustomAlertDialog(
284 | modifier: Modifier = Modifier,
285 | titleString: String,
286 | titleIcon: ImageVector,
287 | opened: MutableState,
288 | dismissButton: @Composable (() -> Unit)? = null,
289 | content: @Composable (() -> Unit)? = null,
290 | ) {
291 | AlertDialog(
292 | modifier = modifier
293 | .fillMaxWidth(),
294 | //.wrapContentSize(),
295 | title = {
296 | Row {
297 | Icon(
298 | titleIcon,
299 | contentDescription = null,
300 | modifier = Modifier.size(ButtonDefaults.IconSize)
301 | )
302 | Spacer(Modifier.size(ButtonDefaults.IconSpacing))
303 | Text(text = titleString)
304 | }
305 | },
306 | shape = RoundedCornerShape(20.dp),
307 | onDismissRequest = {
308 | opened.value = false
309 | },
310 | text = content,
311 | confirmButton = {
312 | TextButton(
313 | onClick = { opened.value = false }) {
314 | Text(stringResource(id = android.R.string.ok))
315 | }
316 | },
317 | dismissButton = dismissButton,
318 | )
319 | }
320 |
321 | /**
322 | * Preview ui elements in design preview of android studio
323 | * TODO: add more UI elements
324 | */
325 |
326 | @Preview
327 | @Composable
328 | fun PassText() {
329 | Box {
330 | CustomCardBool("True", true)
331 | }
332 | }
333 |
334 | @Preview
335 | @Composable
336 | fun FailText() {
337 | Box {
338 | CustomCardBool("False", false)
339 | }
340 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/CustomCards.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.outlined.Help
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.dp
13 | import com.henrikherzig.playintegritychecker.R
14 |
15 | /**
16 | * settings ui for the request
17 | */
18 | @Composable
19 | fun RequestSettings(
20 | selectedIndexCheck: String,
21 | itemsCheck: List>,
22 | changedCheck: (String) -> Unit,
23 | selectedIndexNonce: String,
24 | itemsNonce: List>,
25 | changedNonce: (String) -> Unit,
26 | ) {
27 | Column(
28 | modifier = Modifier
29 | .padding(12.dp)
30 | .fillMaxWidth(),
31 | ) {
32 | val openedNonce = remember { mutableStateOf(false) }
33 | val openedCheck = remember { mutableStateOf(false) }
34 | CustomCardTitle(text = stringResource(id = R.string.requestSettings_title))
35 | Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
36 | CustomCardTitle3(text = stringResource(id = R.string.requestSettings_nonceCreation))
37 | CustomHelpButton(onClick = {openedNonce.value = true})
38 | if (openedNonce.value) {
39 | CustomTextAlertDialog(
40 | stringResource(id = R.string.requestSettings_nonceCreation),
41 | stringResource(id = R.string.requestSettings_nonceCreation_helpText),
42 | opened = openedNonce,
43 | )
44 | }
45 | }
46 | Spacer(modifier = Modifier.height(8.dp))
47 | ToggleGroup(selectedIndexCheck, itemsCheck, changedCheck, 35.dp)
48 | Spacer(modifier = Modifier.height(12.dp))
49 | Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
50 | CustomCardTitle3(text = stringResource(id = R.string.requestSettings_checkVerdict))
51 | CustomHelpButton(onClick = {openedCheck.value = true})
52 | if (openedCheck.value) {
53 | CustomTextAlertDialog(
54 | stringResource(id = R.string.requestSettings_checkVerdict),
55 | stringResource(id = R.string.requestSettings_checkVerdict_helpText),
56 | opened = openedCheck,
57 | )
58 | }
59 | }
60 | Spacer(modifier = Modifier.height(8.dp))
61 | ToggleGroup(selectedIndexNonce, itemsNonce, changedNonce, 35.dp)
62 | //CustomCard
63 | }
64 | }
65 |
66 | /**
67 | * shows device information in a card
68 | */
69 | @Composable
70 | fun DeviceInfoContent(playServiceVersion: String?) {
71 | CustomCard {
72 | Column(
73 | modifier = Modifier
74 | .padding(12.dp)
75 | .fillMaxWidth(),
76 | ) {
77 | CustomCardTitle(text = stringResource(R.string.info_device))
78 |
79 | CustomCardGroup(
80 | text1 = stringResource(R.string.info_model),
81 | text2 = "${Build.MODEL} (${Build.DEVICE})"
82 | )
83 | CustomCardGroup(
84 | text1 = stringResource(R.string.info_androidVersion),
85 | text2 = "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
86 | )
87 |
88 | CustomCardGroup(
89 | text1 = stringResource(R.string.info_securityPatch),
90 | text2 = Build.VERSION.SECURITY_PATCH
91 | )
92 |
93 | if (playServiceVersion != null) {
94 | CustomCardGroup(
95 | text1 = stringResource(R.string.info_playServicesVersion),
96 | text2 = playServiceVersion
97 | )
98 | }
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/CustomComponents.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import androidx.browser.customtabs.CustomTabsIntent
6 | import androidx.compose.animation.animateColorAsState
7 | import androidx.compose.animation.core.animateDpAsState
8 | import androidx.compose.animation.core.tween
9 | import androidx.compose.foundation.Canvas
10 | import androidx.compose.foundation.layout.*
11 | import androidx.compose.material.*
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.Circle
14 | import androidx.compose.material.icons.outlined.Circle
15 | import androidx.compose.material.icons.outlined.Close
16 | import androidx.compose.material.icons.outlined.Help
17 | import androidx.compose.material.icons.outlined.OpenInNew
18 | import androidx.compose.runtime.*
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.geometry.CornerRadius
22 | import androidx.compose.ui.geometry.Offset
23 | import androidx.compose.ui.geometry.Size
24 | import androidx.compose.ui.graphics.Color
25 | import androidx.compose.ui.res.stringResource
26 | import androidx.compose.ui.unit.Dp
27 | import androidx.compose.ui.unit.dp
28 | import com.google.accompanist.pager.ExperimentalPagerApi
29 | import com.google.accompanist.pager.HorizontalPager
30 | import com.google.accompanist.pager.rememberPagerState
31 | import com.henrikherzig.playintegritychecker.R
32 |
33 | /**
34 | * opens a link in a preview window within the app
35 | */
36 | fun openLink(url: String, context: Context) {
37 | CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(url))
38 | }
39 |
40 | /**
41 | * shows device information in a card
42 | */
43 | @OptIn(ExperimentalMaterialApi::class)
44 | @Composable
45 | fun CustomButtonRow(context: Context, linkIdPairs: List>) {
46 | Row(
47 | modifier = Modifier
48 | .fillMaxWidth(),
49 | horizontalArrangement = Arrangement.spacedBy(16.dp)
50 | ) {
51 | linkIdPairs.forEachIndexed { _, item ->
52 | CompositionLocalProvider(
53 | LocalMinimumTouchTargetEnforcement provides false,
54 | ) {
55 | Box(
56 | modifier = Modifier
57 | .fillMaxWidth()
58 | .weight(1f)
59 | ) {
60 | CustomButton(
61 | { openLink(item.first, context) },
62 | Icons.Outlined.OpenInNew,
63 | item.second
64 | )
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
71 | /**
72 | * shows help card button
73 | */
74 | @Composable
75 | fun CustomHelpButton(onClick: () -> Unit) {
76 | IconButton(
77 | onClick = onClick,
78 | modifier = Modifier.size(ButtonDefaults.IconSize)
79 | ) {
80 | Icon(
81 | Icons.Outlined.Help,
82 | contentDescription = null,
83 | modifier = Modifier.size(ButtonDefaults.IconSize),
84 | tint = MaterialTheme.colors.primary
85 | )
86 | }
87 | }
88 |
89 |
90 | /**
91 | * shows close card button
92 | */
93 | @Composable
94 | fun CustomCloseButton(onClick: () -> Unit) {
95 | IconButton(
96 | onClick = onClick,
97 | modifier = Modifier.size(ButtonDefaults.IconSize)
98 | ) {
99 | Icon(
100 | Icons.Outlined.Close,
101 | contentDescription = null,
102 | modifier = Modifier.size(ButtonDefaults.IconSize),
103 | tint = MaterialTheme.colors.primary
104 | )
105 | }
106 | }
107 |
108 | /**
109 | * shows a Threema inspired three dot element indicating the verdict strength
110 | * [state] = [1,2,3] where 1 is low, 2 is medium and 3 is strong
111 | */
112 | @Composable
113 | fun CustomThreeStateIcons(state: Int, text: String) {
114 | val color = if (text == "MEETS_VIRTUAL_INTEGRITY") Color.Blue else when (state) {
115 | 0 -> Color(0xFFE53935)
116 | 1 -> Color(0xFFF57C00)
117 | 2 -> Color(0xFFFFC107)
118 | 3 -> Color(0xFF7CB342)
119 | else -> Color.Gray
120 | }
121 | Row(verticalAlignment = Alignment.CenterVertically) {
122 | Icon(
123 | if (state > 0) {
124 | Icons.Filled.Circle
125 | } else {
126 | Icons.Outlined.Circle
127 | },
128 | contentDescription = null,
129 | modifier = Modifier.size(ButtonDefaults.IconSize),
130 | tint = color
131 | )
132 | Icon(
133 | if (state > 1) {
134 | Icons.Filled.Circle
135 | } else {
136 | Icons.Outlined.Circle
137 | },
138 | contentDescription = null,
139 | modifier = Modifier.size(ButtonDefaults.IconSize),
140 | tint = color
141 | )
142 | Icon(
143 | if (state > 2) {
144 | Icons.Filled.Circle
145 | } else {
146 | Icons.Outlined.Circle
147 | },
148 | contentDescription = null,
149 | modifier = Modifier.size(ButtonDefaults.IconSize),
150 | tint = color
151 | )
152 | Spacer(modifier = Modifier.width(5.dp))
153 | Text(
154 | text = text,
155 | color = color
156 | )
157 | }
158 | }
159 |
160 |
161 | data class HorizontalPagerContent(
162 | val threeState: Pair,
163 | val text: String,
164 | )
165 |
166 | data class HorizontalPagerContentText(
167 | val title: String,
168 | val description: String
169 | )
170 |
171 | @OptIn(ExperimentalPagerApi::class)
172 | @Composable
173 | fun CustomSlideStackDeviceRecognitionVerdict(initialPage: Int) {
174 |
175 | @Composable
176 | fun createItems() = listOf(
177 | HorizontalPagerContent(
178 | threeState = Pair(0,"NO_INTEGRITY"),
179 | text = stringResource(id = R.string.deviceRecognition_help_NO_INTEGRITY)
180 | ),
181 | HorizontalPagerContent(
182 | threeState = Pair(1,"MEETS_BASIC_INTEGRITY"),
183 | text = stringResource(id = R.string.deviceRecognition_help_MEETS_BASIC_INTEGRITY)
184 | ),
185 | HorizontalPagerContent(
186 | threeState = Pair(2,"MEETS_DEVICE_INTEGRITY"),
187 | text = stringResource(id = R.string.deviceRecognition_help_MEETS_DEVICE_INTEGRITY)
188 | ),
189 | HorizontalPagerContent(
190 | threeState = Pair(3,"MEETS_STRONG_INTEGRITY"),
191 | text = stringResource(id = R.string.deviceRecognition_help_MEETS_STRONG_INTEGRITY)
192 | ),
193 | HorizontalPagerContent(
194 | threeState = Pair(0,"MEETS_VIRTUAL_INTEGRITY"),
195 | text = stringResource(id = R.string.deviceRecognition_help_MEETS_VIRTUAL_INTEGRITY)
196 | )
197 | )
198 |
199 | val items = createItems()
200 |
201 | val pagerState = rememberPagerState(initialPage)
202 | Column(horizontalAlignment = Alignment.CenterHorizontally) {
203 | HorizontalPager(count = items.size, state = pagerState) { page ->
204 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
205 | .fillMaxHeight()
206 | .padding(top = 60.dp, bottom = 30.dp)) {
207 | val item = items[currentPage]
208 | CustomThreeStateIcons(item.threeState.first, item.threeState.second)
209 | Spacer(modifier = Modifier.height(20.dp))
210 | Text(text = item.text)
211 | //Spacer(modifier = Modifier.weight(1f))
212 | }
213 | }
214 | PageIndicator(
215 | numberOfPages = items.size,
216 | selectedPage = pagerState.currentPage,
217 | defaultRadius = 20.dp,
218 | selectedLength = 40.dp,
219 | space = 10.dp,
220 | animationDurationInMillis = 500,
221 | )
222 | }
223 | }
224 |
225 | @OptIn(ExperimentalPagerApi::class)
226 | @Composable
227 | fun CustomSlideStackRecentDeviceActivity(initialPage: Int) {
228 |
229 | @Composable
230 | fun createItems() = listOf(
231 | HorizontalPagerContentText(
232 | title = "LEVEL_1",
233 | description = stringResource(id = R.string.recentDeviceActivity_help_LEVEL_1)
234 | ),
235 | HorizontalPagerContentText(
236 | title = "LEVEL_2",
237 | description = stringResource(id = R.string.recentDeviceActivity_help_LEVEL_2)
238 | ),
239 | HorizontalPagerContentText(
240 | title = "LEVEL_3",
241 | description = stringResource(id = R.string.recentDeviceActivity_help_LEVEL_3)
242 | ),
243 | HorizontalPagerContentText(
244 | title = "LEVEL_4",
245 | description = stringResource(id = R.string.recentDeviceActivity_help_LEVEL_4)
246 | ),
247 | HorizontalPagerContentText(
248 | title = "UNEVALUATED",
249 | description = stringResource(id = R.string.recentDeviceActivity_help_UNEVALUATED)
250 | ),
251 | )
252 |
253 | val items = createItems()
254 |
255 | val pagerState = rememberPagerState(initialPage)
256 | Column(horizontalAlignment = Alignment.CenterHorizontally) {
257 | HorizontalPager(count = items.size, state = pagerState) { page ->
258 | Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
259 | .fillMaxHeight()
260 | .padding(top = 60.dp, bottom = 30.dp)) {
261 | val item = items[currentPage]
262 | CustomCardTitle2(item.title)
263 | Spacer(modifier = Modifier.height(20.dp))
264 | Text(text = item.description)
265 | }
266 | }
267 | PageIndicator(
268 | numberOfPages = items.size,
269 | selectedPage = pagerState.currentPage,
270 | defaultRadius = 20.dp,
271 | selectedLength = 40.dp,
272 | space = 10.dp,
273 | animationDurationInMillis = 500,
274 | )
275 | }
276 | }
277 |
278 |
279 | @Composable
280 | fun PageIndicator(
281 | numberOfPages: Int,
282 | modifier: Modifier = Modifier,
283 | selectedPage: Int = 0,
284 | selectedColor: Color = Color.Blue,
285 | defaultColor: Color = Color.LightGray,
286 | defaultRadius: Dp = 20.dp,
287 | selectedLength: Dp = 60.dp,
288 | space: Dp = 30.dp,
289 | animationDurationInMillis: Int = 300,
290 | ) {
291 | Row(
292 | verticalAlignment = Alignment.CenterVertically,
293 | horizontalArrangement = Arrangement.spacedBy(space),
294 | modifier = modifier,
295 | ) {
296 | for (i in 0 until numberOfPages) {
297 | val isSelected = i == selectedPage
298 | PageIndicatorView(
299 | isSelected = isSelected,
300 | selectedColor = selectedColor,
301 | defaultColor = defaultColor,
302 | defaultRadius = defaultRadius,
303 | selectedLength = selectedLength,
304 | animationDurationInMillis = animationDurationInMillis,
305 | )
306 | }
307 | }
308 | }
309 |
310 | @Composable
311 | fun PageIndicatorView(
312 | isSelected: Boolean,
313 | selectedColor: Color,
314 | defaultColor: Color,
315 | defaultRadius: Dp,
316 | selectedLength: Dp,
317 | animationDurationInMillis: Int,
318 | modifier: Modifier = Modifier,
319 | ) {
320 |
321 | val color: Color by animateColorAsState(
322 | targetValue = if (isSelected) {
323 | selectedColor
324 | } else {
325 | defaultColor
326 | },
327 | animationSpec = tween(
328 | durationMillis = animationDurationInMillis,
329 | )
330 | )
331 | val width: Dp by animateDpAsState(
332 | targetValue = if (isSelected) {
333 | selectedLength
334 | } else {
335 | defaultRadius
336 | },
337 | animationSpec = tween(
338 | durationMillis = animationDurationInMillis,
339 | )
340 | )
341 |
342 | Canvas(
343 | modifier = modifier
344 | .size(
345 | width = width,
346 | height = defaultRadius,
347 | ),
348 | ) {
349 | drawRoundRect(
350 | color = color,
351 | topLeft = Offset.Zero,
352 | size = Size(
353 | width = width.toPx(),
354 | height = defaultRadius.toPx(),
355 | ),
356 | cornerRadius = CornerRadius(
357 | x = defaultRadius.toPx(),
358 | y = defaultRadius.toPx(),
359 | ),
360 | )
361 | }
362 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/CustomTextField.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui
2 |
3 | import androidx.compose.foundation.interaction.MutableInteractionSource
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.foundation.text.BasicTextField
7 | import androidx.compose.foundation.text.KeyboardActions
8 | import androidx.compose.foundation.text.KeyboardOptions
9 | import androidx.compose.material.ExperimentalMaterialApi
10 | import androidx.compose.material.MaterialTheme
11 | import androidx.compose.material.TextFieldDefaults
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.LocalFocusManager
16 | import androidx.compose.ui.text.TextStyle
17 | import androidx.compose.ui.text.input.ImeAction
18 | import androidx.compose.ui.text.input.VisualTransformation
19 | import androidx.compose.ui.unit.dp
20 |
21 | /**
22 | * custom implementation of textField in order to get rid of unnecessary padding
23 | */
24 | @OptIn(ExperimentalMaterialApi::class)
25 | @Composable
26 | fun CustomTextField(
27 | value: String,
28 | modifier: Modifier = Modifier,
29 | hideKeyboard: Boolean = false,
30 | onFocusClear: () -> Unit = {},
31 | onSearch: (String) -> Unit = {},
32 | onValueChange: (String) -> Unit,
33 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
34 | placeholder: @Composable (() -> Unit)? = null,
35 | ) {
36 | val focusManager = LocalFocusManager.current
37 | BasicTextField(
38 | value = value,
39 | onValueChange = onValueChange,
40 | modifier = modifier,
41 | interactionSource = interactionSource,
42 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
43 | keyboardActions = KeyboardActions(onSearch = {
44 | focusManager.clearFocus()
45 | onSearch(value)
46 | }),
47 | enabled = true,
48 | singleLine = true,
49 | textStyle = TextStyle(color = MaterialTheme.colors.primary)
50 | ) { innerTextField ->
51 | TextFieldDefaults.OutlinedTextFieldDecorationBox(
52 | value = value,
53 | innerTextField = innerTextField,
54 | singleLine = true,
55 | enabled = true,
56 | interactionSource = interactionSource,
57 | contentPadding = PaddingValues(horizontal = 12.dp),
58 | visualTransformation = VisualTransformation.None,
59 | placeholder = placeholder,
60 | border = {
61 | TextFieldDefaults.BorderBox(
62 | shape = RoundedCornerShape(8.dp),
63 | enabled = true,
64 | isError = false,
65 | colors = TextFieldDefaults.outlinedTextFieldColors(),
66 | interactionSource = interactionSource,
67 | )
68 | }
69 | )
70 | }
71 | if (hideKeyboard) {
72 | focusManager.clearFocus()
73 | // Call onFocusClear to reset hideKeyboard state to false
74 | onFocusClear()
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/CustomViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui
2 |
3 | import androidx.compose.runtime.MutableState
4 | import androidx.datastore.core.DataStore
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.core.booleanPreferencesKey
7 | import androidx.datastore.preferences.core.edit
8 | import androidx.datastore.preferences.core.stringPreferencesKey
9 | import androidx.lifecycle.MutableLiveData
10 | import androidx.lifecycle.ViewModel
11 | import androidx.lifecycle.viewModelScope
12 | import kotlinx.coroutines.flow.collectLatest
13 | import kotlinx.coroutines.launch
14 |
15 | class CustomViewModel(
16 | private val dataStore: DataStore
17 | ) : ViewModel() {
18 | private val forceDarkModeKey = booleanPreferencesKey("theme")
19 | private val serverURL = stringPreferencesKey("server_url")
20 |
21 | val stateTheme = MutableLiveData(null)
22 | val stateURL: MutableLiveData = MutableLiveData("")
23 |
24 | fun requestTheme() {
25 | viewModelScope.launch {
26 | dataStore.data.collectLatest {
27 | stateTheme.value = it[forceDarkModeKey]
28 | }
29 | }
30 | }
31 |
32 | fun requestURL() {
33 | viewModelScope.launch {
34 | dataStore.data.collectLatest {
35 | stateURL.value = it[serverURL]
36 | }
37 | }
38 | }
39 |
40 | fun switchToUseSystemSettings(isSystemSettings: Boolean) {
41 | viewModelScope.launch {
42 | if (isSystemSettings) {
43 | dataStore.edit {
44 | it.remove(forceDarkModeKey)
45 | }
46 | }
47 | }
48 | }
49 |
50 | fun switchToUseDarkMode(isDarkTheme: Boolean) {
51 | viewModelScope.launch {
52 | dataStore.edit {
53 | it[forceDarkModeKey] = isDarkTheme
54 | }
55 | }
56 | }
57 |
58 | fun setURL(url: String) {
59 | if (url == "") {
60 | removeURL()
61 | return
62 | }
63 | viewModelScope.launch {
64 | dataStore.edit {
65 | it[serverURL] = url
66 | }
67 | }
68 | }
69 |
70 | private fun removeURL() {
71 | viewModelScope.launch {
72 | dataStore.edit {
73 | it.remove(serverURL)
74 | }
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/ResponseType.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui
2 |
3 | import com.henrikherzig.playintegritychecker.attestation.PlayIntegrityStatement
4 | import com.henrikherzig.playintegritychecker.attestation.safetynet.SafetyNetStatement
5 |
6 | sealed class ResponseType {
7 | class SuccessSafetyNet(val value: SafetyNetStatement) : ResponseType()
8 | class SuccessPlayIntegrity(val value: PlayIntegrityStatement) : ResponseType()
9 | //class SuccessGeneric(val value: R) : ResponseType()
10 | class SuccessSimple(val value: String) : ResponseType()
11 | class Failure(val error: Throwable) : ResponseType()
12 | object Loading : ResponseType()
13 | object None : ResponseType()
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/about/AboutPage.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.about
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.rememberScrollState
5 | import androidx.compose.foundation.verticalScroll
6 | import androidx.compose.material.*
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.outlined.Code
9 | import androidx.compose.material.icons.outlined.OpenInNew
10 | import androidx.compose.material.icons.outlined.ReadMore
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.unit.dp
19 | import androidx.navigation.NavHostController
20 | import com.henrikherzig.playintegritychecker.R
21 | import com.henrikherzig.playintegritychecker.ui.*
22 |
23 | @Composable
24 | fun AboutPage(navController: NavHostController) {
25 | val context = LocalContext.current
26 | Box(
27 | modifier = Modifier
28 | .fillMaxWidth()
29 | ) {
30 | Column(
31 | modifier = Modifier
32 | .verticalScroll(rememberScrollState())
33 | //.widthIn(max = 600.dp)
34 | .padding(all = 12.dp)
35 | ) {
36 | CustomCardTitle("About this App")
37 | CustomCard {
38 | Column(
39 | modifier = Modifier
40 | .padding(12.dp)
41 | .fillMaxWidth(),
42 | ) {
43 | Row(verticalAlignment = Alignment.CenterVertically/*, modifier = Modifier.padding(12.dp)*/){
44 | Text(stringResource(id = R.string.about_this_app))
45 | }
46 | }
47 | }
48 | Spacer(Modifier.size(12.dp))
49 |
50 | CustomCardTitle("API Info")
51 | Row(
52 | modifier = Modifier
53 | .fillMaxWidth(),
54 | //.padding(16.dp),
55 | horizontalArrangement = Arrangement.spacedBy(16.dp)
56 | ) {
57 | Box(
58 | modifier = Modifier
59 | .fillMaxWidth()
60 | .weight(1f)
61 | ) {
62 | /* Source Code Link */
63 | val link = stringResource(id = R.string.about_api_playIntegrityLink)
64 | CustomButton(
65 | { openLink(link, context) },
66 | Icons.Outlined.OpenInNew,
67 | stringResource(id = R.string.about_api_playIntegrityButton)
68 | )
69 | }
70 | Box(
71 | modifier = Modifier
72 | .fillMaxWidth()
73 | .weight(1f)
74 | ) {
75 | /* Play Integrity API Link */
76 | val link = stringResource(id = R.string.about_api_safetyNetLink)
77 | CustomButton(
78 | { openLink(link, context) },
79 | Icons.Outlined.OpenInNew,
80 | stringResource(id = R.string.about_api_safetyNetButton)
81 | )
82 | }
83 | }
84 | Spacer(Modifier.size(12.dp))
85 | CustomCardTitle("Source code")
86 | Row(
87 | modifier = Modifier
88 | .fillMaxWidth(),
89 | horizontalArrangement = Arrangement.spacedBy(16.dp)
90 | ) {
91 | /* workaround: .weight is not accessible in button directly and also not if box
92 | is extracted to other method */
93 | Box(
94 | modifier = Modifier
95 | .fillMaxWidth()
96 | .weight(1f)
97 | ) {
98 | /* Source Code Link */
99 | val link = stringResource(id = R.string.about_sourceCode_appLink)
100 | CustomButton(
101 | { openLink(link, context) },
102 | Icons.Outlined.Code,
103 | stringResource(id = R.string.about_sourceCode_appButton)
104 | )
105 | }
106 | Box(
107 | modifier = Modifier
108 | .fillMaxWidth()
109 | .weight(1f)
110 | ) {
111 | /* Play Integrity API Link */
112 | val link = stringResource(id = R.string.about_sourceCode_serverLink)
113 | CustomButton(
114 | { openLink(link, context) },
115 | Icons.Outlined.Code,
116 | stringResource(id = R.string.about_sourceCode_serverButton)
117 | )
118 | }
119 | }
120 | Spacer(Modifier.size(12.dp))
121 | CustomCardTitle(stringResource(id = R.string.about_licenseAndPrivacy))
122 | Row(
123 | modifier = Modifier
124 | .fillMaxWidth(),
125 | //.padding(16.dp),
126 | horizontalArrangement = Arrangement.spacedBy(16.dp)
127 | ) {
128 | Box(
129 | modifier = Modifier
130 | .fillMaxWidth()
131 | .weight(1f)
132 | ) {
133 | /* License */
134 | val openedRecognition = remember { mutableStateOf(false) }
135 | CustomButton(
136 | { openedRecognition.value = true },
137 | Icons.Outlined.ReadMore,
138 | stringResource(id = R.string.about_licenseButton)
139 | )
140 | if (openedRecognition.value) {
141 | openedRecognition.value=false
142 | navController.navigate("licence") {
143 |
144 | }
145 | }
146 | }
147 | Box(
148 | modifier = Modifier
149 | .fillMaxWidth()
150 | .weight(1f)
151 | ) {
152 | /* Privacy */
153 | val link = stringResource(id = R.string.about_privacyLink)
154 | CustomButton(
155 | { openLink(link, context) },
156 | Icons.Outlined.ReadMore,
157 | stringResource(id = R.string.about_privacyButton) // policy
158 | )
159 | }
160 | }
161 |
162 | }
163 | }
164 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/navigationbar/BottomNavItem.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.navigationbar
2 |
3 |
4 | import androidx.annotation.StringRes
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.*
7 | import androidx.compose.material.icons.outlined.ContentCopy
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.vector.ImageVector
10 | import com.henrikherzig.playintegritychecker.R
11 |
12 | sealed class BottomNavItem(@StringRes var title:Int, var icon: ImageVector, var screen_route:String){
13 |
14 | object PlayIntegrity : BottomNavItem(R.string.bottomBar_playIntegrity, Icons.Filled.VerifiedUser ,"play_integrity")
15 | object SafetyNet: BottomNavItem(R.string.bottomBar_safetyNet,Icons.Filled.VerifiedUser,"safety_net")
16 | object Settings: BottomNavItem(R.string.bottomBar_settings, Icons.Filled.Settings,"settings")
17 | object About: BottomNavItem(R.string.bottomBar_about,Icons.Filled.Info,"about")
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/navigationbar/NavigationBarWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.navigationbar
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.rememberScrollState
6 | import androidx.compose.foundation.verticalScroll
7 | import androidx.compose.material.*
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.filled.ArrowBack
10 | import androidx.compose.runtime.*
11 | import androidx.compose.runtime.livedata.observeAsState
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.platform.LocalContext
16 | import androidx.compose.ui.res.painterResource
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.text.style.TextAlign
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.sp
21 | import androidx.navigation.NavDestination.Companion.hierarchy
22 | import androidx.navigation.NavGraph.Companion.findStartDestination
23 | import androidx.navigation.compose.NavHost
24 | import androidx.navigation.compose.composable
25 | import androidx.navigation.compose.currentBackStackEntryAsState
26 | import androidx.navigation.compose.rememberNavController
27 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
28 | import com.henrikherzig.playintegritychecker.R
29 | import com.henrikherzig.playintegritychecker.attestation.PlayIntegrityStatement
30 | import com.henrikherzig.playintegritychecker.attestation.safetynet.SafetyNetStatement
31 | import com.henrikherzig.playintegritychecker.dataStore
32 | import com.henrikherzig.playintegritychecker.ui.CustomCardTitle
33 | import com.henrikherzig.playintegritychecker.ui.about.AboutPage
34 | import com.henrikherzig.playintegritychecker.ui.playintegrity.PlayIntegrity
35 | import com.henrikherzig.playintegritychecker.ui.safetynet.SafetyNet
36 | import com.henrikherzig.playintegritychecker.ui.ResponseType
37 | import com.henrikherzig.playintegritychecker.ui.settings.Settings
38 | import com.henrikherzig.playintegritychecker.ui.CustomViewModel
39 | import com.mikepenz.aboutlibraries.ui.compose.LibrariesContainer
40 |
41 | @Composable
42 | fun BottomNavigationBar(
43 | safetyNetResult: State>,
44 | onSafetyNetRequest: (String, String, String?) -> Unit,
45 | playIntegrityResult: State>,
46 | onPlayIntegrityRequest: (String, String, String?) -> Unit,
47 | playServiceVersion: String?
48 | ) {
49 | val systemUiController = rememberSystemUiController()
50 | systemUiController.setSystemBarsColor(color = MaterialTheme.colors.primarySurface)
51 | systemUiController.setNavigationBarColor(color = MaterialTheme.colors.primarySurface)
52 | val navController = rememberNavController()
53 | val appPages = listOf(
54 | BottomNavItem.PlayIntegrity,
55 | BottomNavItem.SafetyNet,
56 | BottomNavItem.Settings,
57 | BottomNavItem.About,
58 | )
59 | val navBackStackEntry by navController.currentBackStackEntryAsState()
60 |
61 | // TODO: very ugly better solution in the future
62 | // check
63 | var selectedIndexCheckPlayIntegrity by remember { mutableStateOf("local") }
64 | var selectedIndexCheckSafetyNet by remember { mutableStateOf("local") }
65 |
66 | val local: String = stringResource(id = R.string.requestSettings_local)
67 | val server: String = stringResource(id = R.string.requestSettings_server)
68 | val google: String = stringResource(id = R.string.requestSettings_google)
69 | val itemsCheck: List> = listOf(listOf("local", local), listOf("server", server))
70 | val changedCheckPlayIntegrity: (idx: String) -> Unit = {
71 | selectedIndexCheckPlayIntegrity = it
72 | }
73 | val changedCheckSafetyNet: (idx: String) -> Unit = {
74 | selectedIndexCheckSafetyNet = it
75 | }
76 | var selectedIndexNoncePlayIntegrity by remember { mutableStateOf("local") }
77 | var selectedIndexNonceSafetyNet by remember { mutableStateOf("local") }
78 |
79 | val itemsNonce: List> =
80 | listOf(listOf("local", local), listOf("server", server), listOf("google", google))
81 | val changedNoncePlayIntegrity: (idx: String) -> Unit = {
82 | selectedIndexNoncePlayIntegrity = it
83 | }
84 | val changedNonceSafetyNet: (idx: String) -> Unit = {
85 | selectedIndexNonceSafetyNet = it
86 | }
87 |
88 | // Better state handling: Use ViewModels
89 | val context = LocalContext.current
90 |
91 | val viewModel = remember {
92 | CustomViewModel(context.dataStore)
93 | }
94 | // url
95 | LaunchedEffect(viewModel) {
96 | viewModel.requestURL()
97 | }
98 | val urlValue = viewModel.stateURL.observeAsState().value
99 |
100 | val appBarHorizontalPadding = 0.dp
101 | val titleIconModifier = Modifier
102 | .fillMaxHeight()
103 | .width(72.dp - appBarHorizontalPadding)
104 |
105 | Scaffold(
106 | topBar = {
107 | TopAppBar(
108 | /*
109 | * app bar title of the app
110 | * first line: short app name variant in bigger font
111 | * second line: full app name variant in smaller font
112 | */
113 | modifier= Modifier.fillMaxWidth(),
114 | title = {
115 | // TopAppBar Content
116 | Box(Modifier.height(40.dp)) {
117 |
118 | // Navigation Icon
119 | if (navBackStackEntry?.destination?.route == "licence") {
120 | Row(titleIconModifier, verticalAlignment = Alignment.CenterVertically) {
121 | IconButton(
122 | onClick = { navController.navigateUp() },
123 | enabled = true,
124 | ) {
125 | Icon(
126 | imageVector = Icons.Filled.ArrowBack,
127 | contentDescription = "Back",
128 | )
129 | }
130 | }
131 | }
132 |
133 | // Title
134 | Row(Modifier.fillMaxSize(),
135 | verticalAlignment = Alignment.CenterVertically) {
136 | Column(
137 | modifier = Modifier.fillMaxSize(),
138 | verticalArrangement = Arrangement.Center,
139 | ) {
140 | Text(
141 | text = stringResource(id = R.string.app_name_short),
142 | style = MaterialTheme.typography.subtitle2,
143 | fontSize = 18.sp,
144 | modifier = Modifier.align(Alignment.CenterHorizontally)
145 | )
146 | Text(
147 | text = stringResource(id = R.string.app_name),
148 | style = MaterialTheme.typography.caption,
149 | modifier = Modifier.align(Alignment.CenterHorizontally)
150 | )
151 |
152 | }
153 |
154 | }
155 |
156 | // Actions
157 | /* CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
158 | Row(
159 | Modifier.fillMaxHeight(),
160 | horizontalArrangement = Arrangement.End,
161 | verticalAlignment = Alignment.CenterVertically,
162 | ){}
163 | } */
164 | }
165 | },
166 | )
167 | },
168 | bottomBar = {
169 | BottomNavigation {
170 | // val navBackStackEntry by navController.currentBackStackEntryAsState()
171 | val currentDestination = navBackStackEntry?.destination
172 | appPages.forEach { screen ->
173 | BottomNavigationItem(
174 | icon = { Icon(screen.icon, contentDescription = null) },
175 | label = { Text(stringResource(screen.title)) },
176 | selected = currentDestination?.hierarchy?.any { it.route == screen.screen_route } == true,
177 | onClick = {
178 | navController.navigate(screen.screen_route) {
179 | // Pop up to the first page to avoid large stack
180 | popUpTo(navController.graph.findStartDestination().id) {
181 | saveState = true
182 | }
183 | // Avoid multiple versions of same page
184 | launchSingleTop = true
185 | // Restore state when selecting a previous page again
186 | // restoreState = true
187 | }
188 | }
189 | )
190 | }
191 | }
192 | },
193 |
194 | ) { innerPadding ->
195 | NavHost(
196 | navController,
197 | startDestination = BottomNavItem.PlayIntegrity.screen_route,
198 | Modifier.padding(innerPadding)
199 | ) {
200 | composable(BottomNavItem.PlayIntegrity.screen_route) {
201 | PlayIntegrity(
202 | playIntegrityResult,
203 | {
204 | onPlayIntegrityRequest(
205 | selectedIndexCheckPlayIntegrity,
206 | selectedIndexNoncePlayIntegrity,
207 | urlValue
208 | )
209 | },
210 | selectedIndexCheckPlayIntegrity,
211 | itemsCheck,
212 | changedCheckPlayIntegrity,
213 | selectedIndexNoncePlayIntegrity,
214 | itemsNonce,
215 | changedNoncePlayIntegrity
216 | )
217 | }
218 | composable(BottomNavItem.SafetyNet.screen_route) {
219 | SafetyNet(
220 | safetyNetResult,
221 | {
222 | onSafetyNetRequest(
223 | selectedIndexCheckSafetyNet,
224 | selectedIndexNonceSafetyNet,
225 | urlValue
226 | )
227 | },
228 | selectedIndexCheckSafetyNet,
229 | itemsCheck,
230 | changedCheckSafetyNet,
231 | selectedIndexNonceSafetyNet,
232 | itemsNonce.subList(0, 2),
233 | changedNonceSafetyNet
234 | )
235 | }
236 | composable(BottomNavItem.Settings.screen_route) {
237 | Settings(playServiceVersion)
238 | }
239 | composable(BottomNavItem.About.screen_route) {
240 | AboutPage(navController)
241 | }
242 | composable("licence") {
243 | // Licenses Page
244 | Column(
245 | modifier = Modifier
246 | .padding(all = 12.dp)
247 | ) {
248 | CustomCardTitle(stringResource(id = R.string.about_licenseButton))
249 | LibrariesContainer()
250 | }
251 | }
252 | }
253 | }
254 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/playintegrity/PlayIntegrityPage.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.playintegrity
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.browser.customtabs.CustomTabsIntent
6 | import androidx.compose.animation.AnimatedVisibility
7 | import androidx.compose.animation.ExperimentalAnimationApi
8 | import androidx.compose.animation.animateContentSize
9 | import androidx.compose.foundation.layout.*
10 | import androidx.compose.foundation.rememberScrollState
11 | import androidx.compose.foundation.verticalScroll
12 | import androidx.compose.material.*
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.outlined.GppGood
15 | import androidx.compose.runtime.*
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.unit.dp
20 | import androidx.core.content.ContextCompat
21 | import com.henrikherzig.playintegritychecker.R
22 | import com.henrikherzig.playintegritychecker.attestation.PlayIntegrityStatement
23 | import com.henrikherzig.playintegritychecker.ui.*
24 |
25 | /**
26 | * returns the main ui of the App
27 | * @param playIntegrityResult api request result. Can be called with a initial value [ResponseType.None] as long
28 | * as no api request has been made
29 | * @param onPlayIntegrityRequest function to trigger when the API request is being made
30 | */
31 | @Composable
32 | fun PlayIntegrity(
33 | playIntegrityResult: State>,
34 | onPlayIntegrityRequest: () -> Unit,
35 | selectedIndexCheck: String,
36 | itemsCheck: List>,
37 | changedCheck: (String) -> Unit,
38 | selectedIndexNonce: String,
39 | itemsNonce: List>,
40 | changedNonce: (String) -> Unit,
41 | ) {
42 | val context = LocalContext.current
43 |
44 | /**
45 | * Opens the Link external within primary browser
46 | * function not used at the moment
47 | */
48 | fun openLinkExternal(url: String) {
49 | // shorter alternative
50 | // val uriHandler = LocalUriHandler.current
51 | // uriHandler.openUri(url)
52 |
53 | runCatching {
54 | ContextCompat.startActivity(
55 | context,
56 | Intent(Intent.ACTION_VIEW).setData(Uri.parse(url)),
57 | null
58 | )
59 | }
60 | }
61 |
62 | /**
63 | * opens a link in a preview window within the app
64 | */
65 | fun openLink(url: String) {
66 | CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(url))
67 | }
68 |
69 | Box(
70 | modifier = Modifier
71 | .fillMaxWidth()
72 | ) {
73 | Column(
74 | modifier = Modifier
75 | .verticalScroll(rememberScrollState())
76 | //.widthIn(max = 600.dp)
77 | .padding(all = 12.dp)
78 | ) {
79 | CustomCard() {
80 | RequestSettings(
81 | selectedIndexCheck,
82 | itemsCheck,
83 | changedCheck,
84 | selectedIndexNonce,
85 | itemsNonce,
86 | changedNonce
87 | )
88 | }
89 | Spacer(modifier = Modifier.height(12.dp))
90 |
91 | /* "Make Play Integrity Request" button */
92 | CustomButton(
93 | onPlayIntegrityRequest,
94 | Icons.Outlined.GppGood,
95 | stringResource(id = R.string.playIntegrity_button)
96 | )
97 |
98 | Spacer(modifier = Modifier.height(8.dp))
99 |
100 | // determines weather it is necessary to show the card with the API request result or not
101 | val showPlayIntegrityContent = playIntegrityResult.value != ResponseType.None
102 |
103 | /* Result Card (only visible, when button was pressed) */
104 | AnimatedVisibility(visible = showPlayIntegrityContent) {
105 | Box(
106 | // Animation of box getting bigger when API result is shown
107 | modifier = Modifier.animateContentSize()
108 | ) {
109 | CustomCard {
110 | ResultContent(playIntegrityResult) {
111 | //playIntegrityResult.value = ResponseType.None
112 | }
113 | }
114 | }
115 | }
116 | }
117 | }
118 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/safetynet/SafetyNetPage.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.safetynet
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.animateContentSize
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.outlined.GppGood
10 | import androidx.compose.runtime.*
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.unit.dp
15 | import com.henrikherzig.playintegritychecker.attestation.safetynet.SafetyNetStatement
16 | import com.henrikherzig.playintegritychecker.R
17 | import com.henrikherzig.playintegritychecker.ui.CustomButton
18 | import com.henrikherzig.playintegritychecker.ui.CustomCard
19 | import com.henrikherzig.playintegritychecker.ui.RequestSettings
20 | import com.henrikherzig.playintegritychecker.ui.ResultContent
21 | import com.henrikherzig.playintegritychecker.ui.ResponseType
22 |
23 | /**
24 | * returns the main ui of the App
25 | * @param safetyNetResult api request result. Can be called with a initial value [ResponseType.None] as long
26 | * as no api request has been made
27 | * @param onSafetyNetRequest function to trigger when the API request is being made
28 | */
29 | @Composable
30 | fun SafetyNet(
31 | safetyNetResult: State>,
32 | onSafetyNetRequest: () -> Unit,
33 | selectedIndexCheck: String,
34 | itemsCheck: List>,
35 | changedCheck: (String) -> Unit,
36 | selectedIndexNonce: String,
37 | itemsNonce: List>,
38 | changedNonce: (String) -> Unit,
39 | ) {
40 | Box(
41 | modifier = Modifier
42 | .fillMaxWidth()
43 | ) {
44 | /* Main column in which every other UI element for the main screen is in */
45 | Column(
46 | modifier = Modifier
47 | .verticalScroll(rememberScrollState())
48 | //.widthIn(max = 600.dp)
49 | .padding(all = 12.dp)
50 | ) {
51 | CustomCard {
52 | RequestSettings(
53 | selectedIndexCheck,
54 | itemsCheck,
55 | changedCheck,
56 | selectedIndexNonce,
57 | itemsNonce,
58 | changedNonce
59 | )
60 | }
61 | Spacer(modifier = Modifier.height(12.dp))
62 | /* "Make SafetyNet AttestationAPI Request" button */
63 | CustomButton(
64 | onSafetyNetRequest,
65 | Icons.Outlined.GppGood,
66 | stringResource(id = R.string.safetyNet_attestation_button)
67 | )
68 |
69 | Spacer(modifier = Modifier.height(8.dp))
70 |
71 | // determines weather it is necessary to show the card with the API request result or not
72 | val showSafetyNetContent = safetyNetResult.value != ResponseType.None
73 |
74 | /* Result Card (only visible, when button was pressed) */
75 | AnimatedVisibility(visible = showSafetyNetContent) {
76 | Box(
77 | // Animation of box getting bigger when API result is shown
78 | modifier = Modifier.animateContentSize()
79 | ) {
80 | CustomCard {
81 | ResultContent(safetyNetResult){
82 | //playIntegrityResult.value = ResponseType.None
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/settings/SettingsPage.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.settings
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.interaction.MutableInteractionSource
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.rememberScrollState
7 | import androidx.compose.foundation.verticalScroll
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.*
10 | import androidx.compose.runtime.livedata.observeAsState
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.dp
16 | import com.henrikherzig.playintegritychecker.R
17 | import com.henrikherzig.playintegritychecker.dataStore
18 | import com.henrikherzig.playintegritychecker.ui.*
19 |
20 | @Composable
21 | fun Settings(playServiceVersion: String?) {
22 | // get context, viewModel and stateTheme observer for detecting/setting ui changes
23 | val context = LocalContext.current
24 | val viewModel = remember { CustomViewModel(context.dataStore) }
25 | val themeValue = viewModel.stateTheme.observeAsState().value
26 |
27 | // variable to store state of theme selector
28 | val themeSelectorMode by remember(themeValue) {
29 | val mode = when (themeValue) {
30 | null -> "system"
31 | true -> "dark"
32 | false -> "light"
33 | }
34 | mutableStateOf(mode)
35 | }
36 | // theme selector options
37 | val system: String = stringResource(id = R.string.settings_theme_system)
38 | val light: String = stringResource(id = R.string.settings_theme_light)
39 | val dark: String = stringResource(id = R.string.settings_theme_dark)
40 |
41 | val themeSelectorOptions: List> =
42 | listOf(listOf("system", system), listOf("light", light), listOf("dark", dark))
43 | val modeChanged: (String) -> Unit = {
44 | when (it) {
45 | "system" -> viewModel.switchToUseSystemSettings(true)
46 | "dark" -> viewModel.switchToUseDarkMode(true)
47 | "light" -> viewModel.switchToUseDarkMode(false)
48 | }
49 | }
50 |
51 | // update viewModel variables on change
52 | LaunchedEffect(viewModel) {
53 | viewModel.requestTheme()
54 | viewModel.requestURL()
55 | }
56 |
57 | // get url variable for textField
58 | val urlValue = viewModel.stateURL.observeAsState().value
59 |
60 | // variable for textField to set serverURL
61 | var text by remember(urlValue) {
62 | val text = when (urlValue) {
63 | null -> ""
64 | else -> urlValue
65 | }
66 | mutableStateOf(text)
67 | }
68 |
69 | // if clicked outside of the TextInputField for entering ServerURL the focus to it should be lost
70 | val interactionSource = MutableInteractionSource()
71 | var hideKeyboard by remember { mutableStateOf(false) }
72 |
73 | // define settings UI
74 | Box(
75 | modifier = Modifier
76 | .fillMaxWidth()
77 | .fillMaxHeight()
78 | .clickable( // no animation when clicked to lose focus of url textField
79 | interactionSource = interactionSource,
80 | indication = null
81 | ) { hideKeyboard = true }
82 | ) {
83 | Column(
84 | modifier = Modifier
85 | .verticalScroll(rememberScrollState())
86 | .padding(all = 12.dp)
87 | ) {
88 | DeviceInfoContent(playServiceVersion)
89 | Spacer(Modifier.size(12.dp))
90 | CustomCardTitle(stringResource(R.string.settings_title))
91 | Row(
92 | verticalAlignment = Alignment.CenterVertically,
93 | modifier = Modifier
94 | // .fillMaxWidth()
95 | .padding(horizontal = 8.dp),
96 | //horizontalArrangement = Arrangement.spacedBy(16.dp)
97 | ) {
98 | /* workaround: .weight is not accessible in button directly and also not if box
99 | is extracted to other method, have to investigate this */
100 | Box(
101 | modifier = Modifier
102 | .fillMaxWidth(fraction = 0.3f)
103 | //.weight(0.3f)
104 | //.absolutePadding(right = 12.dp)
105 | ) {
106 | Text(stringResource(R.string.settings_theme))
107 | }
108 | Box(
109 | modifier = Modifier
110 | .fillMaxWidth()
111 | .weight(1f)
112 | //.absolutePadding(left = 12.dp)
113 | ) {
114 | ToggleGroup(themeSelectorMode, themeSelectorOptions, modeChanged, 35.dp)
115 | }
116 | }
117 | Spacer(modifier = Modifier.size(12.dp))
118 | Row(
119 | verticalAlignment = Alignment.CenterVertically,
120 | modifier = Modifier
121 | // .fillMaxWidth()
122 | .padding(horizontal = 8.dp),
123 | //horizontalArrangement = Arrangement.spacedBy(16.dp)
124 | ) {
125 | /* workaround: .weight is not accessible in button directly and also not if box
126 | is extracted to other method, have to investigate this */
127 | Box(
128 | modifier = Modifier
129 | .fillMaxWidth(fraction = 0.3f)
130 | //.weight(0.3f)
131 | //.absolutePadding(right = 12.dp)
132 | ) {
133 | Text(stringResource(R.string.settings_url))
134 | }
135 | CustomTextField(
136 | value = text,
137 | onValueChange = {
138 | text = it
139 | },
140 | hideKeyboard = hideKeyboard,
141 | onFocusClear = {
142 | viewModel.setURL(text)
143 | hideKeyboard = false
144 | },
145 | onSearch = {
146 | viewModel.setURL(text)
147 | },
148 | modifier = Modifier
149 | .fillMaxWidth()
150 | //.padding(8.dp)
151 | .height(35.dp),
152 | placeholder = { Text(stringResource(R.string.settings_url_hint)) }
153 | )
154 | }
155 | }
156 | }
157 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.LaunchedEffect
9 | import androidx.compose.runtime.livedata.observeAsState
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.ui.platform.LocalContext
12 | import com.henrikherzig.playintegritychecker.dataStore
13 | import com.henrikherzig.playintegritychecker.ui.CustomViewModel
14 |
15 | private val DarkColorPalette = darkColors(
16 | primary = Purple200,
17 | primaryVariant = Purple700,
18 | secondary = Teal200,
19 | //surface = Color(0xFF121212)
20 | )
21 |
22 | private val LightColorPalette = lightColors(
23 | primary = Purple500,
24 | primaryVariant = Purple700,
25 | secondary = Teal200
26 |
27 | /* Other default colors to override
28 | background = Color.White,
29 | surface = Color.White,
30 | onPrimary = Color.White,
31 | onSecondary = Color.Black,
32 | onBackground = Color.Black,
33 | onSurface = Color.Black,
34 | */
35 | )
36 |
37 | @Composable
38 | fun PlayIntegrityCheckerTheme(
39 | //darkTheme: Boolean = isSystemInDarkTheme(),
40 | content: @Composable () -> Unit
41 | ) {
42 | val context = LocalContext.current
43 | val viewModel = remember { CustomViewModel(context.dataStore) }
44 | val state = viewModel.stateTheme.observeAsState()
45 | val value = state.value ?: isSystemInDarkTheme()
46 | LaunchedEffect(viewModel) { viewModel.requestTheme() }
47 | val colors = if (value) {
48 | DarkColorPalette
49 | } else {
50 | LightColorPalette
51 | }
52 |
53 | MaterialTheme(
54 | colors = colors,
55 | typography = Typography,
56 | shapes = Shapes,
57 | content = content
58 | )
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/henrikherzig/playintegritychecker/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-de-rDE/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Play Integrity Checker
4 | SPIC
5 |
6 |
7 | SafetyNet Attestation Ergebnis:
8 | Nonce
9 | Zeitstempel
10 | Auswertungstyp
11 | Einfache Integrität
12 | CTS Profil Übereinstimmung
13 | APK Paket Name
14 | APK Digest (Sha256)
15 | APK Zertifikat Digest (Sha256)
16 |
17 | Hardwaregestützt
18 | Einfach
19 | bestanden
20 | fehlgeschlagen
21 | unausgewertet
22 |
23 | SafetyNet Attestation Anfrage
24 |
25 |
26 |
27 | Play Integrity Ergebnis:
28 |
29 | Anfragendetails
30 | Nonce
31 | Anfragen Paket Name
32 | Zeitstempel
33 |
34 | App Integrität
35 | App Erkennungs Urteil
36 | APK Paket Name
37 | APK Zertifikat Digest (Sha256)
38 | Version Code
39 |
40 | Geräteintegrität
41 | Gerät Erkennungs Urteil
42 | Aktuelle Geräteaktivität
43 |
44 | Account Details
45 | App Lizensierungs Urteil
46 |
47 | Umgebungsdetails
48 | Play Protect Urteil
49 |
50 | Play Integrity Anfrage
51 |
52 | The device has signs of an attack (such as API hooking) or system compromise (such as being rooted), or it is not a physical device (such as an emulator that does not pass Google Play integrity checks).
53 | The device passes basic system integrity checks. It may not meet Android compatibility requirements and may not be approved to run Google Play services (eg. device is running unrecognized version of Android, may have an unlocked bootloader, or may not have been certified by the manufacturer).
54 | The Android device is powered by Google Play services. It passes system integrity checks and meets Android compatibility requirements.
55 | The Android device is powered by Google Play services and has a strong guarantee of system integrity such as a hardware-backed proof of boot integrity. It passes system integrity checks and meets Android compatibility requirements.
56 | The app is running on an Android emulator powered by Google Play services. The emulator passes system integrity checks and meets core Android compatibility requirements.
57 |
58 | Niedrigste Stufe: Die App hat in der letzten Stunde 10 oder weniger Integritätstoken auf diesem Gerät angefordert.
59 | Die App hat in der letzten Stunde zwischen 11 und 25 Integritätstoken auf diesem Gerät angefordert.
60 | Die App hat in der letzten Stunde zwischen 26 und 50 Integritätstoken auf diesem Gerät angefordert.
61 | Höchste Stufe: Die App hat in der letzten Stunde mehr als 50 Integritätstoken auf diesem Gerät angefordert.
62 |
63 | Die aktuelle Geräteaktivität wurde nicht ausgewertet. Hier sind einige der verschiedenen Gründe, warum dies passieren könnte:\n\n
64 | \u2022 Das Gerät ist nicht zuverlässig genug.\n
65 | \u2022 Die auf dem Gerät installierte Version Ihrer App ist Google Play nicht bekannt.\n
66 | \u2022 Das Gerät weist technische Probleme auf.
67 |
68 |
69 |
70 | Reines JSON
71 | JSON Exportieren
72 | lädt
73 | Reines JSON Ergebnis
74 | ein unbekannter Fehler ist aufgetreten
75 |
76 | Einstellungen zur Anfrage
77 | Nonce Erstellung
78 | Urteil überprüfen
79 | Lokal
80 | Server
81 | Google
82 |
83 | Determines if the nonce is created on the device locally or on the secure remote server. (Locally generated nonce are only for testing purposes and not recommended in production use). The usage of a nonce prevents replay attacks.
84 | Determines if the integrity verdict should be checked locally on the device or on a secure remote server. (Checking the verdict locally is only for testing purposes as it makes bypassing the check trivial).
85 |
86 | Urteilsstärke:
87 |
88 |
89 | Geräteinfo
90 | Modell
91 | Android Version
92 | Sicherheits-Patch
93 | Play Services Version
94 | Einstellungen
95 | Aussehen
96 | System
97 | Hell
98 | Dunkel
99 | Server URL
100 | eigene server url
101 |
102 |
103 |
104 | Diese App ist eine OpenSource Implementierung der Google Play Integrity API als auch der Safety Net Attestation API (API Informationen siehe unten). Die einzigartige Nonce Erstellung als auch die Überprüfung des Integritätsurteils beider APIs kann sowohl lokal auf dem Gerät als auch auf einem remote server, welcher die Serverimplementierung verwendet, geschehen.
105 |
106 | API Info
107 | Play Integrity
108 | SafetyNet
109 |
110 | Source Code
111 | Android App
112 | Check Server
113 |
114 | Lizenz und Privatsphäre
115 | Lizenzen
116 | Privatsphäre
117 |
118 |
119 | PlayIntegrity
120 | SafetyNet
121 | Einstellungen
122 | Über
123 |
124 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Play Integrity Checker
4 | SPIC
5 |
6 | SafetyNet 認証結果:
7 | ノンス
8 | タイムスタンプ
9 | 評価タイプ
10 | 基本的な完全性
11 | CTS プロファイルの一致
12 | APK パッケージ名
13 | APK ダイジェスト (SHA256)
14 | APK ダイジェスト証明書 (SHA256)
15 | ハードウェア証明済み
16 | Basic
17 | パス
18 | 失敗
19 | 評価なし
20 | SafetyNet 認証をリクエスト
21 |
22 | Play Integrity 認証結果:
23 | リクエストの詳細
24 | ノンス
25 | リクエストパッケージ名
26 | タイムスタンプ
27 | アプリの完全性
28 | アプリの完全性判定
29 | APK パッケージ名
30 | APK ダイジェスト証明書 (SHA256)
31 | バージョンコード
32 | デバイスの完全性
33 | デバイスの完全性判定
34 | アカウントの詳細
35 | アプリライセンスの完全性判定
36 | Play Integrity 認証をリクエスト
37 | デバイスの攻撃(API フッキングなど)やシステム侵害(root 化など)の兆候がある、または物理的なデバイスではない物(Google Play Integrity のチェックをパスしていないエミュレーターなど)
38 | デバイスが基本的なシステムの完全性のチェックにパスしています。Android の互換性要件を満たしていない可能性があり、Google Play サービスの実行が認定されていない可能性があります。(例: 非認識の環境で Android を実行、ブートローダーがアンロック、製造元によって非認定)
39 | Android デバイスは Google Play サービスを搭載しており、システムの完全性のチェックをパスと Android の互換性要件を満たしています。
40 | Android デバイスは Google Play サービスを搭載しており、ブートの完全性をハードウェアで証明するなど、システムの完全性を強力に保証しています。システムの完全性のチェックをパスと Android の互換性要件を満たしています。
41 | このアプリは、Google Play サービスを使用した Android エミュレーター上で動作しています。エミュレーターは、システムの完全性のチェックにパスをしており、Android のコアの互換性要件を満たしています。
42 |
43 | JSON を表示
44 | JSON をエクスポート
45 | 読み込み中
46 | 認証結果の JSON
47 | 不明なエラーが発生しました
48 | リクエストの設定
49 | ノンスの作成
50 | 完全性判定の確認
51 | ローカル
52 | サーバー
53 | Google
54 | ノンスをデバイスでローカル内に作成するか、セキュアなリモートサーバー上で作成するかを決定します。(ローカルで作成されたノンスはテスト用であり、本番での使用は推奨しません)ノンスを使用する事で、リプレイアタックを防ぐ事ができます。
55 | 完全性の判定をデバイスのローカル内でチェックをするか、完全なリモートサーバー上でチェックをするかを決定します。(ローカルでの判定はチェックを迂回する事が簡単になるため、テスト目的に限られます)
56 | 完全性の強度:
57 |
58 | デバイス情報
59 | モデル
60 | Android バージョン
61 | セキュリティパッチ
62 | Play サービスバージョン
63 | 設定
64 | テーマ
65 | システム
66 | ライト
67 | ダーク
68 | サーバー URL
69 | カスタムサーバー URL
70 |
71 | このアプリは、Google Play Integrity API と Safety Net Attestation API のオープンソース実装です。一意のノンス作成と両方の API から返される完全性のチェックは、デバイスのローカル上で実行かサーバー実装を使用してリモートサーバー上で実行する事ができます。
72 | API 情報
73 | Play Integrity
74 | https://developer.android.com/google/play/integrity
75 | SafetyNet
76 | https://developer.android.com/training/safetynet/attestation
77 | ソースコード
78 | Android アプリ
79 | https://github.com/herzhenr/spic-android
80 | サーバーを確認
81 | https://github.com/herzhenr/spic-server
82 | ライセンスとプライバシーポリシー
83 | ライセンス
84 | プライバシー
85 | https://github.com/herzhenr/spic-android/blob/main/privacyPolicy.md
86 |
87 | Play Integ.
88 | SafetyNet
89 | 設定
90 | アプリ情報
91 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Play Integrity Checker
4 | SPIC
5 |
6 |
7 | Resultado do SafetyNet:
8 | Nonce
9 | Data e hora
10 | Tipo de avaliação
11 | Integridade básica
12 | Correspondência de perfil CTS
13 | Nome do pacote APK
14 | Resumo do APK (SHA256)
15 | Resumo do certificado APK (SHA256)
16 | Suporte de hardware
17 | Básico
18 | passou
19 | falhou
20 | não avaliado
21 | Testar SafetyNet
22 |
23 |
24 | Resultado do Play Integrity:
25 | Solicitar detalhes
26 | Nonce
27 | Solicitar nome do pacote
28 | Data e hora
29 | Integridade do app
30 | Veredicto de reconhecimento do app
31 | Nome do pacote APK
32 | Resumo do certificado APK (SHA256)
33 | Código da versão
34 | Integridade do dispositivo
35 | Veredicto de reconhecimento do dispositivo
36 | Detalhes da conta
37 | Veredicto de licenciamento do app
38 | Testar Play Integrity
39 | O dispositivo apresenta sinais de ataque (como interceptação de API) ou comprometimento do sistema (como root) ou não é um dispositivo físico (como um emulador que não passa nas verificações de integridade do Google Play).
40 | O dispositivo passa nas verificações básicas de integridade do sistema. Ele pode não atender aos requisitos de compatibilidade do Android e pode não ser aprovado para executar o Google Play Services (por exemplo, o dispositivo está executando uma versão não reconhecida do Android, pode ter um bootloader desbloqueado ou pode não ter sido certificado pelo fabricante).
41 | O dispositivo Android é alimentado por Google Play Services. Ele passa nas verificações de integridade do sistema e atende aos requisitos de compatibilidade do Android.
42 | O dispositivo Android é alimentado pelo Google Play Services e tem uma forte garantia de integridade do sistema, como uma prova de integridade de inicialização apoiada por hardware. Ele passa nas verificações de integridade do sistema e atende aos requisitos de compatibilidade do Android.
43 | O app está sendo executado em um emulador Android desenvolvido pelo Google Play Services. O emulador passa nas verificações de integridade do sistema e atende aos principais requisitos de compatibilidade do Android.
44 |
45 |
46 | JSON bruto
47 | Exportar JSON
48 | carregando
49 | Resultado do JSON bruto
50 | ocorreu um erro desconhecido
51 | Configurações de solicitação
52 | Criação nonce
53 | Verificar veredicto
54 | local
55 | servidor
56 | google
57 | Determina se o nonce é criado localmente no dispositivo ou no servidor remoto seguro. (Os nonce gerados localmente são apenas para fins de teste e não são recomendados para uso em produção). O uso de um nonce evita ataques de repetição.
58 | Determina se o veredicto de integridade deve ser verificado localmente no dispositivo ou em um servidor remoto seguro. (Verificar o veredicto localmente é apenas para fins de teste, pois torna trivial ignorar a verificação).
59 | Força do veredicto:
60 |
61 |
62 | Informações do dispositivo
63 | Modelo
64 | Versão do Android
65 | Patch de segurança
66 | Versão do Google Play Services
67 | Configurações
68 | Tema
69 | sistema
70 | claro
71 | escuro
72 | URL do servidor
73 | URL do servidor personalizado
74 |
75 |
76 | Este app é uma implementação de código aberto da API Google Play Integrity, bem como da API Safety Net Attestation (informações da API, veja abaixo). A criação de nonce exclusivo, bem como as verificações do veredicto retornado por ambas as APIs, podem ser realizadas localmente no dispositivo ou em um servidor remoto usando a implementação do servidor.
77 | Informações da API
78 | Play Integrity
79 | SafetyNet
80 | Código fonte
81 | App Android
82 | Verificar servidor
83 | Licença e política de privacidade
84 | Licenças
85 | Privacidade
86 |
87 |
88 | Play Integ.
89 | SafetyNet
90 | Configurações
91 | Sobre
92 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3DDC84
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Simple Play Integrity Checker
4 | SPIC
5 |
6 |
7 | SafetyNet Attestation Result:
8 | Nonce
9 | Timestamp
10 | Evaluation Type
11 | Basic Integrity
12 | CTS Profile Match
13 | APK Package Name
14 | APK Digest (Sha256)
15 | APK Certificate Digest (Sha256)
16 |
17 | Hardware Backed
18 | Basic
19 | passed
20 | failed
21 | unevaluated
22 |
23 | Make SafetyNet Attestation Request
24 |
25 |
26 |
27 | Play Integrity Result:
28 |
29 | Request Details
30 | Nonce
31 | Request Package Name
32 | Timestamp
33 |
34 | App Integrity
35 | App recognition verdict
36 | APK Package Name
37 | APK Certificate Digest (Sha256)
38 | Version Code
39 |
40 | Device Integrity
41 | Device recognition verdict
42 | Recent device activity
43 |
44 | Account Details
45 | App licensing verdict
46 |
47 | Environment Details
48 | Play Protect Verdict
49 |
50 | Make Play Integrity Request
51 |
52 | The device has signs of an attack (such as API hooking) or system compromise (such as being rooted), or it is not a physical device (such as an emulator that does not pass Google Play integrity checks).
53 | The device passes basic system integrity checks. It may not meet Android compatibility requirements and may not be approved to run Google Play services (eg. device is running unrecognized version of Android, may have an unlocked bootloader, or may not have been certified by the manufacturer).
54 | The Android device is powered by Google Play services. It passes system integrity checks and meets Android compatibility requirements.
55 | The Android device is powered by Google Play services and has a strong guarantee of system integrity such as a hardware-backed proof of boot integrity. It passes system integrity checks and meets Android compatibility requirements.
56 | The app is running on an Android emulator powered by Google Play services. The emulator passes system integrity checks and meets core Android compatibility requirements.
57 |
58 | Lowest level: The app requested 10 or fewer integrity tokens on this device in the last hour.
59 | The app requested between 11 and 25 integrity tokens on this device in the last hour.
60 | The app requested between 26 and 50 integrity tokens on this device in the last hour.
61 | Highest level: The app requested more than 50 integrity tokens on this device in the last hour.
62 |
63 | Recent device activity was not evaluated. This could happen for several reasons, including the following:\n\n
64 | \u2022 The device is not trustworthy enough.\n
65 | \u2022 The version of your app installed on the device is unknown to Google Play.\n
66 | \u2022 Technical issues on the device.
67 |
68 |
69 |
70 | Raw JSON
71 | Export JSON
72 | loading
73 | Raw JSON result
74 | unknown error occurred
75 |
76 | Request Settings
77 | Nonce Creation
78 | Check Verdict
79 | local
80 | server
81 | google
82 |
83 | Determines if the nonce is created on the device locally or on the secure remote server. (Locally generated nonce are only for testing purposes and not recommended in production use). The usage of a nonce prevents replay attacks.
84 | Determines if the integrity verdict should be checked locally on the device or on a secure remote server. (Checking the verdict locally is only for testing purposes as it makes bypassing the check trivial).
85 |
86 | Verdict Strength:
87 |
88 |
89 | Device Info
90 | Model
91 | Android Version
92 | Security Patch
93 | Play Services Version
94 | Settings
95 | Theme
96 | system
97 | light
98 | dark
99 | Server URL
100 | custom server url
101 |
102 |
103 |
104 | This app is an open source implementation of the Google Play Integrity API as well as the Safety Net Attestation API (API info see below). The unique nonce creation as well as the checks of the verdict returned by both APIs can be performed locally on the device or on a remote server using the server implementation.
105 |
106 | API Info
107 | Play Integrity
108 | https://developer.android.com/google/play/integrity
109 | SafetyNet
110 | https://developer.android.com/training/safetynet/attestation
111 |
112 | Source Code
113 | Android App
114 | https://github.com/herzhenr/spic-android
115 | Check Server
116 | https://github.com/herzhenr/spic-server
117 |
118 | License and privacy policy
119 | Licenses
120 | Privacy
121 | https://github.com/herzhenr/spic-android/blob/main/privacyPolicy.md
122 |
123 |
124 | Play Integ.
125 | SafetyNet
126 | Settings
127 | About
128 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/henrikherzig/playintegritychecker/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.henrikherzig.playintegritychecker
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_version = '1.3.0-beta01'
4 | accompanist_version = '0.24.12-rc'
5 | }
6 | repositories {
7 | google() // maven { url "https://maven.google.com" } for Gradle <= 3
8 | }
9 | dependencies {
10 | classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6'
11 | }
12 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
13 | plugins {
14 | id 'com.android.application' version '7.2.2' apply false
15 | id 'com.android.library' version '7.2.2' apply false
16 | id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
17 | id 'com.mikepenz.aboutlibraries.plugin' version "10.5.2" apply false
18 | }
19 |
20 | task clean(type: Delete) {
21 | delete rootProject.buildDir
22 | }
23 |
24 | //allprojects {
25 | // tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
26 | // kotlinOptions {
27 | // freeCompilerArgs += [
28 | // "-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
29 | // ]
30 | // }
31 | // }
32 | //}
--------------------------------------------------------------------------------
/docs/aboutLight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/docs/aboutLight.png
--------------------------------------------------------------------------------
/docs/googlePlay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/docs/googlePlay.png
--------------------------------------------------------------------------------
/docs/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/docs/icon.png
--------------------------------------------------------------------------------
/docs/playIntegrityLight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/docs/playIntegrityLight.png
--------------------------------------------------------------------------------
/docs/safetyNetJsonDark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/docs/safetyNetJsonDark.png
--------------------------------------------------------------------------------
/docs/settingsLight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/docs/settingsLight.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 |
25 | android.enableJetifier=true
26 |
27 | # Gradle properties for module app
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/herzhenr/spic-android/9d0a78479702b190a7e7c43db1a4d657f73ad378/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jul 22 15:27:39 CEST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/privacyPolicy.md:
--------------------------------------------------------------------------------
1 | **Privacy Policy**
2 |
3 | Henrik Herzig built the Simple Play Integrity Checker app as an Open Source app. This SERVICE is provided by Henrik Herzig at no cost and is intended for use as is.
4 |
5 | This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.
6 |
7 | If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy.
8 |
9 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which are accessible at Simple Play Integrity Checker unless otherwise defined in this Privacy Policy.
10 |
11 | **Information Collection and Use**
12 |
13 | For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request will be retained on your device and is not collected by me in any way.
14 |
15 | The app does use third-party services that may collect information used to identify you.
16 |
17 | Link to the privacy policy of third-party service providers used by the app
18 |
19 | * [Google Play Services](https://www.google.com/policies/privacy/)
20 |
21 | **Log Data**
22 |
23 | I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.
24 |
25 | **Cookies**
26 |
27 | Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
28 |
29 | This Service does not use these “cookies” explicitly. However, the app may use third-party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service.
30 |
31 | **Service Providers**
32 |
33 | I may employ third-party companies and individuals due to the following reasons:
34 |
35 | * To facilitate our Service;
36 | * To provide the Service on our behalf;
37 | * To perform Service-related services; or
38 | * To assist us in analyzing how our Service is used.
39 |
40 | I want to inform users of this Service that these third parties have access to their Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.
41 |
42 | **Security**
43 |
44 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.
45 |
46 | **Links to Other Sites**
47 |
48 | This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
49 |
50 | **Children’s Privacy**
51 |
52 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do the necessary actions.
53 |
54 | **Changes to This Privacy Policy**
55 |
56 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page.
57 |
58 | This policy is effective as of 2023-01-11
59 |
60 | **Contact Us**
61 |
62 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at dev.homebrewed@google.com.
63 |
64 | This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.nisrulz.com/)
65 |
66 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "PlayIntegrityChecker"
16 | include ':app'
17 |
--------------------------------------------------------------------------------