diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 19bae6cca..4774c7644 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/java/com/aurora/extensions/Signature.kt b/app/src/main/java/com/aurora/extensions/Signature.kt new file mode 100644 index 000000000..b68985110 --- /dev/null +++ b/app/src/main/java/com/aurora/extensions/Signature.kt @@ -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 +} diff --git a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt index 61ff988e5..313f79d0c 100644 --- a/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/DownloadWorker.kt @@ -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 { + // 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, libPackageName: String?): List { val downloadList = mutableListOf() files.filter { it.url.isNotBlank() }.forEach { diff --git a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt index daaa537d6..24325b9a3 100644 --- a/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt +++ b/app/src/main/java/com/aurora/store/data/work/UpdateWorker.kt @@ -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()) { diff --git a/app/src/main/java/com/aurora/store/util/CertUtil.kt b/app/src/main/java/com/aurora/store/util/CertUtil.kt index a8c7ec8eb..8d17c64ed 100644 --- a/app/src/main/java/com/aurora/store/util/CertUtil.kt +++ b/app/src/main/java/com/aurora/store/util/CertUtil.kt @@ -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 { - val certificates: MutableList = 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 { + 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 { + 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) } } } diff --git a/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt b/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt index 29d37e60a..d80f2e0c1 100644 --- a/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt +++ b/app/src/main/java/com/aurora/store/viewmodel/all/UpdatesViewModel.kt @@ -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) }