Updates: Use appropriate certificate to verify and purchase apps

* Only show updates for devices that has atleast one matching certificate
* On Android 9.0+ devices, purchase apps with latest certificate's hash while updating
  to support key rotations

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-12-29 14:16:33 +05:30
parent 8f788dc01f
commit e66c558f82
6 changed files with 100 additions and 61 deletions

View File

@@ -159,7 +159,7 @@ dependencies {
implementation("com.github.topjohnwu.libsu:core:5.0.5") implementation("com.github.topjohnwu.libsu:core:5.0.5")
//Love <3 //Love <3
implementation("com.aurora:gplayapi:3.2.8") implementation("com.aurora:gplayapi:3.2.9")
//Browser //Browser
implementation("androidx.browser:browser:1.7.0") implementation("androidx.browser:browser:1.7.0")

View File

@@ -0,0 +1,12 @@
package com.aurora.extensions
import android.content.pm.Signature
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
fun Signature.generateX509Certificate(): X509Certificate {
val certificateFactory = CertificateFactory.getInstance("X509")
return certificateFactory.generateCertificate(
this.toByteArray().inputStream()
) as X509Certificate
}

View File

@@ -12,6 +12,7 @@ import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.aurora.extensions.copyTo import com.aurora.extensions.copyTo
import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isQAndAbove import com.aurora.extensions.isQAndAbove
import com.aurora.extensions.requiresObbDir import com.aurora.extensions.requiresObbDir
import com.aurora.gplayapi.helpers.PurchaseHelper import com.aurora.gplayapi.helpers.PurchaseHelper
@@ -23,6 +24,7 @@ import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.room.download.Download import com.aurora.store.data.room.download.Download
import com.aurora.store.data.room.download.DownloadDao import com.aurora.store.data.room.download.DownloadDao
import com.aurora.store.util.CertUtil
import com.aurora.store.util.DownloadWorkerUtil import com.aurora.store.util.DownloadWorkerUtil
import com.aurora.store.util.NotificationUtil import com.aurora.store.util.NotificationUtil
import com.aurora.store.util.PathUtil import com.aurora.store.util.PathUtil
@@ -52,6 +54,7 @@ class DownloadWorker @AssistedInject constructor(
private lateinit var download: Download private lateinit var download: Download
private lateinit var notificationManager: NotificationManager private lateinit var notificationManager: NotificationManager
private lateinit var icon: Bitmap private lateinit var icon: Bitmap
private lateinit var purchaseHelper: PurchaseHelper
private val NOTIFICATION_ID = 200 private val NOTIFICATION_ID = 200
@@ -80,7 +83,7 @@ class DownloadWorker @AssistedInject constructor(
// Purchase the app (free apps needs to be purchased too) // Purchase the app (free apps needs to be purchased too)
val authData = AuthProvider.with(appContext).getAuthData() val authData = AuthProvider.with(appContext).getAuthData()
val purchaseHelper = PurchaseHelper(authData) purchaseHelper = PurchaseHelper(authData)
.using(HttpClient.getPreferredClient(appContext)) .using(HttpClient.getPreferredClient(appContext))
notificationManager = notificationManager =
@@ -88,7 +91,7 @@ class DownloadWorker @AssistedInject constructor(
// Bail out if file list is empty // Bail out if file list is empty
download.fileList = download.fileList.ifEmpty { download.fileList = download.fileList.ifEmpty {
purchaseHelper.purchase(download.packageName, download.versionCode, download.offerType) purchase(download.packageName, download.versionCode, download.offerType)
} }
if (download.fileList.isEmpty()) { if (download.fileList.isEmpty()) {
Log.i(TAG, "Nothing to download!") Log.i(TAG, "Nothing to download!")
@@ -112,9 +115,7 @@ class DownloadWorker @AssistedInject constructor(
download.versionCode, download.versionCode,
it.packageName it.packageName
).mkdirs() ).mkdirs()
it.fileList = it.fileList.ifEmpty { it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) }
purchaseHelper.purchase(it.packageName, it.versionCode, 0)
}
requestList.addAll(getDownloadRequest(it.fileList, it.packageName)) requestList.addAll(getDownloadRequest(it.fileList, it.packageName))
} }
} }
@@ -185,6 +186,20 @@ class DownloadWorker @AssistedInject constructor(
} }
} }
private fun purchase(packageName: String, versionCode: Int, offerType: Int): List<GPlayFile> {
// Android 9.0+ supports key rotation, so purchase with latest certificate's hash
return if (isPAndAbove() && download.isInstalled) {
purchaseHelper.purchase(
packageName,
versionCode,
offerType,
CertUtil.getEncodedCertificateHashes(appContext, download.packageName).last()
)
} else {
purchaseHelper.purchase(packageName, versionCode, offerType)
}
}
private fun getDownloadRequest(files: List<GPlayFile>, libPackageName: String?): List<Request> { private fun getDownloadRequest(files: List<GPlayFile>, libPackageName: String?): List<Request> {
val downloadList = mutableListOf<Request>() val downloadList = mutableListOf<Request>()
files.filter { it.url.isNotBlank() }.forEach { files.filter { it.url.isNotBlank() }.forEach {

View File

@@ -28,6 +28,7 @@ import com.aurora.store.R
import com.aurora.store.data.network.HttpClient import com.aurora.store.data.network.HttpClient
import com.aurora.store.data.providers.AuthProvider import com.aurora.store.data.providers.AuthProvider
import com.aurora.store.data.providers.BlacklistProvider import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.CertUtil
import com.aurora.store.util.DownloadWorkerUtil import com.aurora.store.util.DownloadWorkerUtil
import com.aurora.store.util.Log import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil import com.aurora.store.util.PackageUtil
@@ -151,6 +152,13 @@ class UpdateWorker @AssistedInject constructor(
} else { } else {
false false
} }
}.filter { app ->
app.certificateSetList.any {
it.certificateSet in CertUtil.getEncodedCertificateHashes(
appContext,
app.packageName
)
}
} }
if (updatesList.isNotEmpty()) { if (updatesList.isNotEmpty()) {

View File

@@ -20,73 +20,69 @@
package com.aurora.store.util package com.aurora.store.util
import android.content.Context import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Base64
import android.util.Log
import com.aurora.extensions.generateX509Certificate
import com.aurora.extensions.isPAndAbove import com.aurora.extensions.isPAndAbove
import com.aurora.store.util.PackageUtil.getPackageInfo import com.aurora.store.util.PackageUtil.getPackageInfo
import java.io.ByteArrayInputStream import java.security.MessageDigest
import java.io.InputStream
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.Locale import java.util.Locale
object CertUtil { object CertUtil {
private const val FDROID = "FDROID"
private const val GUARDIAN = "GUARDIANPROJECT.INFO"
private fun getX509Certificates( private val TAG = "CertUtil"
context: Context, private val fdroidSubjects = listOf("FDROID", "GUARDIANPROJECT.INFO")
packageName: String
): List<X509Certificate> {
val certificates: MutableList<X509Certificate> = mutableListOf()
try {
val packageInfo = if (isPAndAbove()) {
getPackageInfo(context, packageName, PackageManager.GET_SIGNING_CERTIFICATES)
} else {
getPackageInfo(context, packageName, PackageManager.GET_SIGNATURES)
}
val certificateFactory = CertificateFactory.getInstance("X509")
if (isPAndAbove()) {
packageInfo.signingInfo.apkContentsSigners.forEach {
val bytes = it.toByteArray()
val inputStream: InputStream = ByteArrayInputStream(bytes)
certificates.add(
certificateFactory!!.generateCertificate(inputStream) as X509Certificate
)
}
} else {
for (i in packageInfo.signatures.indices) {
val bytes = packageInfo.signatures[i].toByteArray()
val inStream: InputStream = ByteArrayInputStream(bytes)
certificates.add(
certificateFactory!!.generateCertificate(inStream) as X509Certificate
)
}
}
} catch (e: Exception) {
}
return certificates
}
fun isFDroidApp(context: Context, packageName: String): Boolean { fun isFDroidApp(context: Context, packageName: String): Boolean {
val certificates = getX509Certificates(context, packageName) return getX509Certificates(
context,
packageName
).any { it.subjectDN.name.uppercase(Locale.getDefault()) in fdroidSubjects }
}
return if (certificates.isEmpty()) fun getEncodedCertificateHashes(context: Context, packageName: String): List<String> {
false return try {
else { val certificates = getX509Certificates(context, packageName)
val cert = certificates[0] certificates.map {
val messageDigest = MessageDigest.getInstance("SHA")
if (cert.subjectDN != null) { messageDigest.update(it.encoded)
val DN = cert.subjectDN.name.uppercase(Locale.getDefault()) Base64.encodeToString(
DN.contains(FDROID) || DN.contains(GUARDIAN) messageDigest.digest(),
} else { Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP
false )
} }
} catch (exception: Exception) {
Log.e(TAG, "Failed to get SHA256 certificate hash", exception)
emptyList()
}
}
private fun getX509Certificates(context: Context, packageName: String): List<X509Certificate> {
return try {
val packageInfo = getPackageInfoWithSignature(context, packageName)
if (isPAndAbove()) {
if (packageInfo.signingInfo.hasMultipleSigners()) {
packageInfo.signingInfo.apkContentsSigners.map { it.generateX509Certificate() }
} else {
packageInfo.signingInfo.signingCertificateHistory.map { it.generateX509Certificate() }
}
} else {
packageInfo.signatures.map { it.generateX509Certificate() }
}
} catch (exception: Exception) {
Log.e(TAG, "Failed to get X509 certificates", exception)
emptyList()
}
}
private fun getPackageInfoWithSignature(context: Context, packageName: String): PackageInfo {
return if (isPAndAbove()) {
getPackageInfo(context, packageName, PackageManager.GET_SIGNING_CERTIFICATES)
} else {
getPackageInfo(context, packageName, PackageManager.GET_SIGNATURES)
} }
} }
} }

View File

@@ -24,6 +24,7 @@ import android.util.Log
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.App
import com.aurora.store.util.CertUtil
import com.aurora.store.util.DownloadWorkerUtil import com.aurora.store.util.DownloadWorkerUtil
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.Locale import java.util.Locale
@@ -58,6 +59,13 @@ class UpdatesViewModel @Inject constructor(
} else { } else {
false false
} }
}.filter { app ->
app.certificateSetList.any {
it.certificateSet in CertUtil.getEncodedCertificateHashes(
getApplication(),
app.packageName
)
}
}.sortedBy { it.displayName.lowercase(Locale.getDefault()) }.also { apps -> }.sortedBy { it.displayName.lowercase(Locale.getDefault()) }.also { apps ->
_updates.emit(apps) _updates.emit(apps)
} }