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")
//Love <3
implementation("com.aurora:gplayapi:3.2.8")
implementation("com.aurora:gplayapi:3.2.9")
//Browser
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.WorkerParameters
import com.aurora.extensions.copyTo
import com.aurora.extensions.isPAndAbove
import com.aurora.extensions.isQAndAbove
import com.aurora.extensions.requiresObbDir
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.room.download.Download
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.NotificationUtil
import com.aurora.store.util.PathUtil
@@ -52,6 +54,7 @@ class DownloadWorker @AssistedInject constructor(
private lateinit var download: Download
private lateinit var notificationManager: NotificationManager
private lateinit var icon: Bitmap
private lateinit var purchaseHelper: PurchaseHelper
private val NOTIFICATION_ID = 200
@@ -80,7 +83,7 @@ class DownloadWorker @AssistedInject constructor(
// Purchase the app (free apps needs to be purchased too)
val authData = AuthProvider.with(appContext).getAuthData()
val purchaseHelper = PurchaseHelper(authData)
purchaseHelper = PurchaseHelper(authData)
.using(HttpClient.getPreferredClient(appContext))
notificationManager =
@@ -88,7 +91,7 @@ class DownloadWorker @AssistedInject constructor(
// Bail out if file list is empty
download.fileList = download.fileList.ifEmpty {
purchaseHelper.purchase(download.packageName, download.versionCode, download.offerType)
purchase(download.packageName, download.versionCode, download.offerType)
}
if (download.fileList.isEmpty()) {
Log.i(TAG, "Nothing to download!")
@@ -112,9 +115,7 @@ class DownloadWorker @AssistedInject constructor(
download.versionCode,
it.packageName
).mkdirs()
it.fileList = it.fileList.ifEmpty {
purchaseHelper.purchase(it.packageName, it.versionCode, 0)
}
it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) }
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> {
val downloadList = mutableListOf<Request>()
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.providers.AuthProvider
import com.aurora.store.data.providers.BlacklistProvider
import com.aurora.store.util.CertUtil
import com.aurora.store.util.DownloadWorkerUtil
import com.aurora.store.util.Log
import com.aurora.store.util.PackageUtil
@@ -151,6 +152,13 @@ class UpdateWorker @AssistedInject constructor(
} else {
false
}
}.filter { app ->
app.certificateSetList.any {
it.certificateSet in CertUtil.getEncodedCertificateHashes(
appContext,
app.packageName
)
}
}
if (updatesList.isNotEmpty()) {

View File

@@ -20,73 +20,69 @@
package com.aurora.store.util
import android.content.Context
import android.content.pm.PackageInfo
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.store.util.PackageUtil.getPackageInfo
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.cert.CertificateFactory
import java.security.MessageDigest
import java.security.cert.X509Certificate
import java.util.Locale
object CertUtil {
private const val FDROID = "FDROID"
private const val GUARDIAN = "GUARDIANPROJECT.INFO"
private fun getX509Certificates(
context: Context,
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
}
private val TAG = "CertUtil"
private val fdroidSubjects = listOf("FDROID", "GUARDIANPROJECT.INFO")
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())
false
else {
val cert = certificates[0]
if (cert.subjectDN != null) {
val DN = cert.subjectDN.name.uppercase(Locale.getDefault())
DN.contains(FDROID) || DN.contains(GUARDIAN)
} else {
false
fun getEncodedCertificateHashes(context: Context, packageName: String): List<String> {
return try {
val certificates = getX509Certificates(context, packageName)
certificates.map {
val messageDigest = MessageDigest.getInstance("SHA")
messageDigest.update(it.encoded)
Base64.encodeToString(
messageDigest.digest(),
Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP
)
}
} 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.lifecycle.viewModelScope
import com.aurora.gplayapi.data.models.App
import com.aurora.store.util.CertUtil
import com.aurora.store.util.DownloadWorkerUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import java.util.Locale
@@ -58,6 +59,13 @@ class UpdatesViewModel @Inject constructor(
} else {
false
}
}.filter { app ->
app.certificateSetList.any {
it.certificateSet in CertUtil.getEncodedCertificateHashes(
getApplication(),
app.packageName
)
}
}.sortedBy { it.displayName.lowercase(Locale.getDefault()) }.also { apps ->
_updates.emit(apps)
}