/*
* Copyright (C) 2019-2021 E FOUNDATION
* Copyright (C) 2021 ECORP SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package foundation.e.apps.application.model
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.os.AsyncTask
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import foundation.e.apps.R
import foundation.e.apps.api.FDroidAppExistsRequest
import foundation.e.apps.application.model.data.FullData
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.PGPCompressedData
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
import org.bouncycastle.openpgp.PGPSignatureList
import org.bouncycastle.openpgp.PGPUtil
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
import org.json.JSONException
import org.json.JSONObject
import java.io.*
import java.security.MessageDigest
import java.security.Security
class IntegrityVerificationTask(
private val applicationInfo: ApplicationInfo,
private val fullData: FullData,
private val integrityVerificationCallback: IntegrityVerificationCallback
) :
AsyncTask() {
private lateinit var systemJsonData: JSONObject
private var verificationSuccessful: Boolean = false
private var TAG = "IntegrityVerificationTask"
override fun doInBackground(vararg context: Context): Context {
try {
val packageName = getAPK_PackageName(context[0])
verificationSuccessful = if (isSystemApplication(context[0], packageName.toString())) {
verifyAPKSignature(context[0])
} else if (isfDroidApplication(packageName.toString())) {
verifyFdroidSignature(context[0])
} else {
checkGoogleApp(context[0])
}
} catch (e: Exception) {
e.printStackTrace()
}
return context[0]
}
private fun checkGoogleApp(context: Context): Boolean {
return verifyShaSum(context)
}
private fun verifyShaSum(context: Context): Boolean {
if (!fullData.getLastVersion()!!.apkSHA.isNullOrEmpty()) {
return getApkFileSha1(applicationInfo.getApkOrXapkFile(context, fullData, fullData.basicData)) ==
fullData.getLastVersion()!!.apkSHA
}
return false
}
private fun verifyAPKSignature(context: Context): Boolean {
if (getAPKSignature(context) != null) {
return getAPKSignature(context)?.toCharsString() ==
getSystemSignature(context.packageManager)?.toCharsString()
}
return false
}
private fun getAPKSignature(context: Context): Signature? {
try {
val fullPath: String = applicationInfo.getApkFile(
context,
fullData.basicData
).absolutePath
val releaseSig = context.packageManager.getPackageArchiveInfo(fullPath, PackageManager.GET_SIGNATURES)
return getFirstSignature(releaseSig)
} catch (e: PackageManager.NameNotFoundException) {
Log.d(TAG, "Unable to find the package: android")
}
return null
}
private fun getAPK_PackageName(context: Context): String? {
val pm: PackageManager = context.packageManager
val fullPath: String = applicationInfo.getApkFile(
context,
fullData.basicData
).absolutePath
val info = pm.getPackageArchiveInfo(fullPath, 0)
if (info != null) {
return info.packageName
} else
return null
}
private fun getFirstSignature(pkg: PackageInfo?): Signature? {
return if (pkg?.signatures != null && pkg.signatures.isNotEmpty()) {
pkg.signatures[0]
} else null
}
private fun getSystemSignature(pm: PackageManager): Signature? {
try {
val sys = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES)
return getFirstSignature(sys)
} catch (e: PackageManager.NameNotFoundException) {
Log.d(TAG, "Unable to find the package: android")
}
return null
}
private fun verifyFdroidSignature(context: Context): Boolean {
Security.addProvider(BouncyCastleProvider())
return verifyAPKSignature(
context,
BufferedInputStream(
FileInputStream(
applicationInfo.getApkFile(
context,
fullData.basicData
).absolutePath
)
),
fullData.getLastVersion()!!.signature.byteInputStream(Charsets.UTF_8),
context.assets.open("f-droid.org-signing-key.gpg")
)
}
private fun isfDroidApplication(packageName: String): Boolean {
var fDroidAppExistsResponse = 0
FDroidAppExistsRequest(packageName)
.request { applicationError, searchResult ->
when (applicationError) {
null -> {
if (searchResult.size > 0) {
fDroidAppExistsResponse = searchResult[0]!!
}
}
else -> {
Log.e(TAG, applicationError.toString())
}
}
}
return fDroidAppExistsResponse == 200
}
fun loadJSONFromAsset(context: Context): String? {
var json: String? = null
json = try {
val inputStream: InputStream = context.getAssets().open("systemApp.json")
val size = inputStream.available()
val buffer = ByteArray(size)
inputStream.read(buffer)
inputStream.close()
String(buffer, charset("UTF-8"))
} catch (ex: IOException) {
ex.printStackTrace()
return null
}
return json
}
private fun isSystemApplication(context: Context, packageName: String): Boolean {
var jsonResponse = loadJSONFromAsset(context)
try {
if (JSONObject(jsonResponse).has(packageName)) {
systemJsonData = JSONObject(jsonResponse).getJSONObject(packageName)
return true
}
} catch (e: Exception) {
if (e is JSONException) {
Log.d(TAG, "$packageName is not a system application")
} else {
e.printStackTrace()
}
}
return false
}
override fun onPostExecute(context: Context) {
integrityVerificationCallback.onIntegrityVerified(context, verificationSuccessful)
if (!verificationSuccessful) {
Toast.makeText(context, R.string.not_able_install, Toast.LENGTH_LONG).show()
}
}
private fun getApkFileSha1(file: File): String? {
val messageDigest = MessageDigest.getInstance("SHA-1")
val fileInputStream = FileInputStream(file)
var length = 0
val buffer = ByteArray(8192)
while (length != -1) {
length = fileInputStream.read(buffer)
if (length > 0) {
messageDigest.update(buffer, 0, length)
}
}
return byteArrayToHex(messageDigest.digest())
}
private fun byteArrayToHex(a: ByteArray): String? {
val sb = StringBuilder(a.size * 2)
for (b in a) sb.append(String.format("%02x", b))
return sb.toString()
}
private fun verifyAPKSignature(
context: Context,
apkInputStream: BufferedInputStream,
apkSignatureInputStream: InputStream,
publicKeyInputStream: InputStream
): Boolean {
try {
var jcaPGPObjectFactory =
JcaPGPObjectFactory(PGPUtil.getDecoderStream(apkSignatureInputStream))
val pgpSignatureList: PGPSignatureList
val pgpObject = jcaPGPObjectFactory.nextObject()
if (pgpObject is PGPCompressedData) {
jcaPGPObjectFactory = JcaPGPObjectFactory(pgpObject.dataStream)
pgpSignatureList = jcaPGPObjectFactory.nextObject() as PGPSignatureList
} else {
pgpSignatureList = pgpObject as PGPSignatureList
}
val pgpPublicKeyRingCollection =
PGPPublicKeyRingCollection(
PGPUtil.getDecoderStream(publicKeyInputStream),
JcaKeyFingerprintCalculator()
)
val signature = pgpSignatureList.get(0)
val key = pgpPublicKeyRingCollection.getPublicKey(signature.keyID)
signature.init(BcPGPContentVerifierBuilderProvider(), key)
val buff = ByteArray(1024)
var read = apkInputStream.read(buff)
while (read != -1) {
signature.update(buff, 0, read)
read = apkInputStream.read(buff)
}
apkInputStream.close()
apkSignatureInputStream.close()
publicKeyInputStream.close()
return signature.verify()
} catch (e: Exception) {
e.printStackTrace()
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, context.resources.getString(R.string.Signature_verification_failed), Toast.LENGTH_LONG).show()
}
}
return false
}
}
interface IntegrityVerificationCallback {
fun onIntegrityVerified(context: Context, verificationSuccessful: Boolean)
}