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:
12
app/src/main/java/com/aurora/extensions/Signature.kt
Normal file
12
app/src/main/java/com/aurora/extensions/Signature.kt
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user