diff --git a/LICENSE.material-symbols.txt b/LICENSE.material-symbols.txt index 965226a9..b8472520 100644 --- a/LICENSE.material-symbols.txt +++ b/LICENSE.material-symbols.txt @@ -1,4 +1,5 @@ -A copy of the license for the verified material symbol used in the app icon is provided below. +A copy of the license for the verified material symbol used in the app icon +and material symbols used in the app is provided below. Apache License Version 2.0, January 2004 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c091dcc2..6e61cf37 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,12 +77,12 @@ dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.activity:activity-compose:1.8.2") implementation("androidx.navigation:navigation-compose:2.7.6") + implementation("androidx.datastore:datastore-preferences:1.0.0") + implementation("androidx.compose.material:material-icons-extended") implementation("com.google.android.material:material:1.11.0") implementation("com.google.accompanist:accompanist-drawablepainter:0.33.2-alpha") implementation(platform("androidx.compose:compose-bom:2023.10.01")) implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") - - implementation("androidx.datastore:datastore-preferences:1.0.0") } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/soupslurpr/appverifier/AppVerifier.kt b/app/src/main/kotlin/dev/soupslurpr/appverifier/AppVerifier.kt index 6b98c4a0..6012a87d 100644 --- a/app/src/main/kotlin/dev/soupslurpr/appverifier/AppVerifier.kt +++ b/app/src/main/kotlin/dev/soupslurpr/appverifier/AppVerifier.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import dev.soupslurpr.appverifier.data.InternalDatabaseStatus import dev.soupslurpr.appverifier.preferences.PreferencesViewModel import dev.soupslurpr.appverifier.ui.AppListScreen import dev.soupslurpr.appverifier.ui.CreditsScreen @@ -99,13 +100,14 @@ fun AppVerifierApp( } composable(route = AppVerifierScreens.AppList.name) { AppListScreen( - { name: String, packageName: String, hash: String, icon: Drawable -> - verifyAppViewModel.setAppVerificationInfo(name, packageName, hash) + { name: String, packageName: String, hash: String, icon: Drawable, internalDatabaseStatus: InternalDatabaseStatus -> + verifyAppViewModel.setAppVerificationInfo(name, packageName, hash, internalDatabaseStatus) verifyAppViewModel.setAppIcon(icon) navController.navigate(AppVerifierScreens.VerifyApp.name) }, { verifyAppViewModel.clearUiState() }, - { verifyAppViewModel.getHashHexFromPackageInfo(it) } + { verifyAppViewModel.getHashHexFromPackageInfo(it) }, + { verifyAppViewModel.getInternalDatabaseStatusFromVerificationInfo(it) } ) } composable(route = AppVerifierScreens.VerifyApp.name) { @@ -118,6 +120,7 @@ fun AppVerifierApp( verifyAppUiState.value.appNotFound.value, { verifyAppViewModel.verifyFromText(it) }, verifyAppUiState.value.invalidFormat.value, + verifyAppUiState.value.internalDatabaseStatus.value, ) } composable(route = AppVerifierScreens.Settings.name) { diff --git a/app/src/main/kotlin/dev/soupslurpr/appverifier/InternalVerificationInfoDatabase.kt b/app/src/main/kotlin/dev/soupslurpr/appverifier/InternalVerificationInfoDatabase.kt new file mode 100644 index 00000000..cc950d38 --- /dev/null +++ b/app/src/main/kotlin/dev/soupslurpr/appverifier/InternalVerificationInfoDatabase.kt @@ -0,0 +1,83 @@ +package dev.soupslurpr.appverifier + +import dev.soupslurpr.appverifier.data.VerificationInfo + +val internalVerificationInfoDatabase = setOf( + VerificationInfo( + "dev.soupslurpr.beautyxt", + setOf( + "00:03:01:CC:29:1B:B9:9B:5E:BC:13:BE:89:F0:8C:46:03:80:71:27:B5:5E:21:AA:1E:69:8B:1D:E6:B8:43:46" + ) + ), + VerificationInfo( + "org.thoughtcrime.securesms", + setOf( + "29:F3:4E:5F:27:F2:11:B4:24:BC:5B:F9:D6:71:62:C0:EA:FB:A2:DA:35:AF:35:C1:64:16:FC:44:62:76:BA:26" + ) + ), + VerificationInfo( + "app.accrescent.client", + setOf( + "06:7A:40:C4:19:3A:AD:51:AC:87:F9:DD:FD:EB:B1:5E:24:A1:85:0B:AB:FA:48:21:C2:8C:5C:25:C3:FD:C0:71" + ) + ), + VerificationInfo( + "net.mullvad.mullvadvpn", + setOf( + // Official + "7B:E2:19:30:C3:B4:D7:39:06:B0:89:30:45:0A:1D:3A:FB:D2:2C:98:D9:D8:E9:87:DF:8C:1F:BC:2D:0C:90:BB", + // Google Play Store + "D7:4C:E0:E0:B2:9F:4D:1D:57:AB:F5:EF:7F:9A:37:57:E7:87:CC:A7:A6:25:9B:9C:32:BB:5B:B1:8E:34:63:BD", + // F-Droid + "E1:B6:6A:F1:AC:48:69:A3:3B:09:1F:81:DC:BD:57:7B:F8:DC:FE:91:25:DD:DE:33:81:BF:FF:91:81:33:31:EC", + ) + ), + VerificationInfo( + "com.dominospizza", + setOf( + "97:59:E1:5B:C7:AD:25:FB:A0:5D:43:36:16:E5:1C:E5:04:09:2E:F0:4F:63:C3:61:36:5C:FD:FE:DA:DD:3B:FC" + ) + ), + VerificationInfo( + "com.google.android.GoogleCamera", + setOf( + "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83" + ) + ), + VerificationInfo( + "com.google.android.inputmethod.latin", + setOf( + "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83" + ) + ), + VerificationInfo( + "com.einnovation.temu", + setOf( + "8A:DE:FE:CE:37:49:DC:F2:3C:3C:EB:3A:8B:BB:C9:A1:D8:80:91:B6:76:30:05:88:91:1B:B5:8B:85:97:0B:AF" + ) + ), + VerificationInfo( + "com.zhiliaoapp.musically", + setOf( + "90:41:80:3E:91:BC:B8:14:B4:B4:39:9F:B5:C8:5A:91:64:0B:75:5E:5E:8B:A7:68:13:81:4B:F4:CF:2A:B5:BA" + ) + ), + VerificationInfo( + "com.whatsapp", + setOf( + "39:87:D0:43:D1:0A:EF:AF:5A:87:10:B3:67:14:18:FE:57:E0:E1:9B:65:3C:9D:F8:25:58:FE:B5:FF:CE:5D:44" + ) + ), + VerificationInfo( + "com.squareup.cash", + setOf( + "21:A7:46:75:96:C1:68:65:0F:D7:B6:31:B6:54:22:EB:56:3E:1D:21:AF:F2:2D:DE:73:89:BA:0D:5D:73:87:48" + ) + ), + VerificationInfo( + "im.molly.app", + setOf( + "6A:A8:0F:DF:4A:8C:C1:37:37:CF:B4:34:FC:0C:DE:48:6F:09:CF:8F:CD:A2:1A:67:BE:A5:EE:1C:A2:70:08:86" + ) + ) +) diff --git a/app/src/main/kotlin/dev/soupslurpr/appverifier/data/VerifyAppUiState.kt b/app/src/main/kotlin/dev/soupslurpr/appverifier/data/VerifyAppUiState.kt index d68ccbd2..a86f35cf 100644 --- a/app/src/main/kotlin/dev/soupslurpr/appverifier/data/VerifyAppUiState.kt +++ b/app/src/main/kotlin/dev/soupslurpr/appverifier/data/VerifyAppUiState.kt @@ -13,8 +13,34 @@ data class VerifyAppUiState( val verificationStatus: MutableState = mutableStateOf(VerificationStatus.UNKNOWN), val appNotFound: MutableState = mutableStateOf(false), val invalidFormat: MutableState = mutableStateOf(false), + val internalDatabaseStatus: MutableState = mutableStateOf(InternalDatabaseStatus.NOT_FOUND) ) +enum class InternalDatabaseStatus(val info: String, val simpleInternalDatabaseStatus: SimpleInternalDatabaseStatus) { + NOT_FOUND( + "This app was not found in the internal database. This isn't anything to worry about, but please verify the " + + "app normally.", + SimpleInternalDatabaseStatus.NOT_FOUND, + ), + MATCH( + "This app's verification info matches an entry in the internal database. You don't need to verify normally.", + SimpleInternalDatabaseStatus.SUCCESS, + ), + NOMATCH( + "This app was found in the internal database, but its hash did NOT match. This app may be " + + "non-genuine.", + SimpleInternalDatabaseStatus.FAILURE, + ), +} + +enum class SimpleInternalDatabaseStatus(val color: Color) { + NOT_FOUND(Color.Gray), + SUCCESS(Color.Green), + FAILURE(Color.Red) +} + +data class VerificationInfo(val packageName: String, val hashes: Set) + enum class SimpleVerificationStatus(val color: Color) { UNKNOWN(Color.Gray), SUCCESS(Color.Green), @@ -24,7 +50,7 @@ enum class SimpleVerificationStatus(val color: Color) { enum class VerificationStatus(val info: String, val simpleVerificationStatus: SimpleVerificationStatus) { UNKNOWN( - "Since you haven't provided any verification information, I'm unable to determine your verification status", + "Since you haven't provided any verification information, I'm unable to determine the verification status", SimpleVerificationStatus.UNKNOWN, ), MATCH( diff --git a/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/AppListScreen.kt b/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/AppListScreen.kt index 017365e5..08f6da8f 100644 --- a/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/AppListScreen.kt +++ b/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/AppListScreen.kt @@ -8,6 +8,10 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Verified +import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -16,6 +20,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.google.accompanist.drawablepainter.rememberDrawablePainter +import dev.soupslurpr.appverifier.data.InternalDatabaseStatus +import dev.soupslurpr.appverifier.data.SimpleVerificationStatus +import dev.soupslurpr.appverifier.data.VerificationInfo @Composable fun AppListScreen( @@ -23,10 +30,12 @@ fun AppListScreen( name: String, packageName: String, hash: String, - icon: Drawable + icon: Drawable, + internalDatabusStatus: InternalDatabaseStatus, ) -> Unit, onLaunchedEffect: () -> Unit, getHashHexFromPackageInfo: (packageInfo: PackageInfo) -> String, + getInternalDatabaseStatusFromVerificationInfo: (verification: VerificationInfo) -> InternalDatabaseStatus, ) { val context = LocalContext.current @@ -54,12 +63,15 @@ fun AppListScreen( val hashHex = getHashHexFromPackageInfo(packageInfo) + val verificationInfo = VerificationInfo(packageInfo.packageName, setOf(hashHex)) + AppItem( name = packageManager.getApplicationLabel(packageInfo.applicationInfo).toString(), packageName = packageInfo.packageName, hash = hashHex, icon = packageManager.getApplicationIcon(packageInfo.applicationInfo), onClickAppItem = onClickAppItem, + internalDatabaseStatus = getInternalDatabaseStatusFromVerificationInfo(verificationInfo), ) } } @@ -72,11 +84,18 @@ fun AppItem( packageName: String, hash: String, icon: Drawable, - onClickAppItem: (name: String, packageName: String, hash: String, icon: Drawable) -> Unit, + onClickAppItem: ( + name: String, + packageName: String, + hash: String, + icon: Drawable, + internalDatabaseStatus: InternalDatabaseStatus + ) -> Unit, + internalDatabaseStatus: InternalDatabaseStatus, ) { ListItem( modifier = Modifier.clickable { - onClickAppItem(name, packageName, hash, icon) + onClickAppItem(name, packageName, hash, icon, internalDatabaseStatus) }, headlineContent = { Text(name) @@ -90,6 +109,24 @@ fun AppItem( null, Modifier.size(50.dp), ) + }, + trailingContent = { + when (internalDatabaseStatus) { + InternalDatabaseStatus.NOT_FOUND -> null + InternalDatabaseStatus.MATCH -> Icon( + Icons.Filled.Verified, + "Verified successfully with internal database", + Modifier, + SimpleVerificationStatus.SUCCESS.color, + ) + + InternalDatabaseStatus.NOMATCH -> Icon( + Icons.Filled.Error, + "Verification with internal database NOT successful!", + Modifier, + SimpleVerificationStatus.FAILURE.color, + ) + } } ) } \ No newline at end of file diff --git a/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/CreditsScreen.kt b/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/CreditsScreen.kt index e267a020..44aa7734 100644 --- a/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/CreditsScreen.kt +++ b/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/CreditsScreen.kt @@ -1144,7 +1144,7 @@ fun CreditsScreen() { item { CreditsItem( dependencyName = "Material Symbols", - dependencyPackageName = "", + dependencyPackageName = "androidx.compose.material:material-icons-extended", dependencyLicense = APACHE2LICENSE, ) } diff --git a/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/VerifyAppScreen.kt b/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/VerifyAppScreen.kt index 06d4f2d7..84990246 100644 --- a/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/VerifyAppScreen.kt +++ b/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/VerifyAppScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -38,6 +39,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.startActivity import com.google.accompanist.drawablepainter.rememberDrawablePainter +import dev.soupslurpr.appverifier.data.InternalDatabaseStatus import dev.soupslurpr.appverifier.data.VerificationStatus @Composable @@ -50,6 +52,7 @@ fun VerifyAppScreen( appNotFound: Boolean, onVerifyFromClipboard: (String) -> Unit, invalidFormat: Boolean, + internalDatabaseStatus: InternalDatabaseStatus, ) { val context = LocalContext.current @@ -59,6 +62,8 @@ fun VerifyAppScreen( var showMoreInfoAboutVerificationStatusDialog by rememberSaveable { mutableStateOf(false) } + var showMoreInfoAboutInternalDatabaseStatusDialog by rememberSaveable { mutableStateOf(false) } + Column( modifier = Modifier .fillMaxSize() @@ -80,6 +85,26 @@ fun VerifyAppScreen( "following:\n\ncom.example.app\n96:C0:2C:55:75:5C:17:1C:68:13:70:29:3B:37:11:2B:4A:5D:F7:B9:82:C2:C5:58:05:4C:45:51:AD:F5:50:DC" ) } else { + Text( + "Internal Database Status:" + ) + Row { + FilledTonalButton( + onClick = { showMoreInfoAboutInternalDatabaseStatusDialog = true }, + ) { + Text( + internalDatabaseStatus.simpleInternalDatabaseStatus.name.replace('_', ' '), + style = typography.headlineLarge + ) + Spacer(Modifier.width(8.dp)) + Icon( + Icons.Default.Info, + "More info about internal database status", + tint = internalDatabaseStatus.simpleInternalDatabaseStatus.color, + ) + } + } + Spacer(Modifier.height(8.dp)) if (icon != null) { Image( rememberDrawablePainter(drawable = icon), @@ -143,6 +168,34 @@ fun VerifyAppScreen( } } + if (showMoreInfoAboutInternalDatabaseStatusDialog) { + AlertDialog( + onDismissRequest = { showMoreInfoAboutInternalDatabaseStatusDialog = false }, + confirmButton = { + TextButton( + { showMoreInfoAboutInternalDatabaseStatusDialog = false } + ) { + Text(stringResource(id = android.R.string.ok)) + } + }, + title = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + internalDatabaseStatus.name, + style = typography.headlineSmall, + color = internalDatabaseStatus.simpleInternalDatabaseStatus.color, + ) + } + }, + text = { + Text(internalDatabaseStatus.info) + } + ) + } + if (showMoreInfoAboutVerificationStatusDialog) { AlertDialog( onDismissRequest = { showMoreInfoAboutVerificationStatusDialog = false }, diff --git a/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/VerifyAppViewModel.kt b/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/VerifyAppViewModel.kt index 395448b5..51fea86f 100644 --- a/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/VerifyAppViewModel.kt +++ b/app/src/main/kotlin/dev/soupslurpr/appverifier/ui/VerifyAppViewModel.kt @@ -1,17 +1,21 @@ package dev.soupslurpr.appverifier.ui +import android.app.Application import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable -import androidx.lifecycle.ViewModel +import androidx.lifecycle.AndroidViewModel +import dev.soupslurpr.appverifier.data.InternalDatabaseStatus +import dev.soupslurpr.appverifier.data.VerificationInfo import dev.soupslurpr.appverifier.data.VerificationStatus import dev.soupslurpr.appverifier.data.VerifyAppUiState +import dev.soupslurpr.appverifier.internalVerificationInfoDatabase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import java.security.MessageDigest -class VerifyAppViewModel : ViewModel() { +class VerifyAppViewModel(application: Application) : AndroidViewModel(application) { /** * App verification info @@ -19,14 +23,16 @@ class VerifyAppViewModel : ViewModel() { private val _uiState = MutableStateFlow(VerifyAppUiState()) val uiState: StateFlow = _uiState.asStateFlow() - fun setAppVerificationInfo(name: String, packageName: String, hash: String) { + fun setAppVerificationInfo( + name: String, + packageName: String, + hash: String, + internalDatabaseStatus: InternalDatabaseStatus + ) { _uiState.value.name.value = name _uiState.value.packageName.value = packageName _uiState.value.hash.value = hash - } - - fun clearUiState() { - _uiState.value = VerifyAppUiState() + _uiState.value.internalDatabaseStatus.value = internalDatabaseStatus } fun setAppIcon(icon: Drawable) { @@ -161,6 +167,7 @@ class VerifyAppViewModel : ViewModel() { packageManager.getApplicationLabel(packageInfo.applicationInfo).toString(), packageInfo.packageName, hashHex, + getInternalDatabaseStatusFromVerificationInfo(VerificationInfo(packageName, setOf(hashHex))) ) setAppIcon(packageManager.getApplicationIcon(packageInfo.applicationInfo)) } else { @@ -176,4 +183,32 @@ class VerifyAppViewModel : ViewModel() { fun setInvalidFormat(b: Boolean) { _uiState.value.invalidFormat.value = b } + + fun getInternalDatabaseStatusFromVerificationInfo(verificationInfo: VerificationInfo): InternalDatabaseStatus { + return internalVerificationInfoDatabase.run { + return@run try { + val matchedPackageNameVerificationInfo = this.first { + it.packageName == verificationInfo.packageName + } + + return@run try { + if (verificationInfo.hashes.first { + matchedPackageNameVerificationInfo.hashes.contains(it) + }.isNotEmpty()) { + InternalDatabaseStatus.MATCH + } else { + InternalDatabaseStatus.NOMATCH + } + } catch (e: NoSuchElementException) { + InternalDatabaseStatus.NOMATCH + } + } catch (e: NoSuchElementException) { + InternalDatabaseStatus.NOT_FOUND + } + } + } + + fun clearUiState() { + _uiState.value = VerifyAppUiState() + } } \ No newline at end of file