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:
@@ -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")
|
||||||
|
|||||||
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.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 {
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user