Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit df8bd306 authored by Nihar Thakkar's avatar Nihar Thakkar
Browse files

Verify APK integrity, minor UI improvements

parent b007795d
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -27,7 +27,7 @@ android {

dependencies {
    def lifecycle_version = "1.1.1"
    def work_version = "1.0.0-beta01"
    def work_version = "1.0.0-beta04"
    implementation "android.arch.work:work-runtime:$work_version"
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@@ -45,4 +45,6 @@ dependencies {
    implementation files('libs/jackson-annotations-2.9.7.jar')
    implementation files('libs/jackson-core-2.9.7.jar')
    implementation group: 'commons-codec', name: 'commons-codec', version: '1.11'
    implementation 'org.bouncycastle:bcprov-jdk15on:1.60'
    implementation 'org.bouncycastle:bcpg-jdk15on:1.60'
}
+7.83 KiB

File added.

No diff preview for this file type.

+0 −8
Original line number Diff line number Diff line
@@ -450,16 +450,12 @@ class ApplicationActivity : AppCompatActivity(), ApplicationStateListener,
            app_install.text = resources.getString(state.installButtonTextId)
            when (state) {
                State.INSTALLED -> {
                    app_install.setBackgroundResource(R.drawable.app_install_border)
                    app_install.setTextColor(resources.getColor(android.R.color.primary_text_dark))
                    app_install.isEnabled =
                            Common.appHasLaunchActivity(this, application.packageName)
                    app_size.visibility = View.VISIBLE
                    app_download_container.visibility = View.GONE
                }
                State.DOWNLOADING -> {
                    app_install.setBackgroundResource(R.drawable.app_install_border_simple)
                    app_install.setTextColor(resources.getColor(android.R.color.primary_text_light))
                    app_install.isEnabled = true
                    app_size.visibility = View.GONE
                    app_download_mb.text = getString(R.string.state_installing)
@@ -468,15 +464,11 @@ class ApplicationActivity : AppCompatActivity(), ApplicationStateListener,
                    app_download_container.visibility = View.VISIBLE
                }
                State.INSTALLING -> {
                    app_install.setBackgroundResource(R.drawable.app_install_border)
                    app_install.setTextColor(resources.getColor(android.R.color.primary_text_dark))
                    app_install.isEnabled = false
                    app_size.visibility = View.VISIBLE
                    app_download_container.visibility = View.GONE
                }
                else -> {
                    app_install.setBackgroundResource(R.drawable.app_install_border)
                    app_install.setTextColor(resources.getColor(android.R.color.primary_text_dark))
                    app_install.isEnabled = true
                    app_size.visibility = View.VISIBLE
                    app_download_container.visibility = View.GONE
+16 −22
Original line number Diff line number Diff line
@@ -10,14 +10,11 @@ import foundation.e.apps.utils.Constants
import android.content.Intent
import android.content.BroadcastReceiver
import android.content.IntentFilter
import org.apache.commons.codec.binary.Hex
import java.io.File
import java.io.FileInputStream
import java.lang.Exception
import java.security.MessageDigest
import android.os.AsyncTask

class Downloader(private val applicationInfo: ApplicationInfo, private val fullData: FullData,
                 private val downloaderInterface: DownloaderInterface) {
                 private val downloaderInterface: DownloaderInterface) :
        IntegrityVerificationCallback {
    private lateinit var downloadManager: DownloadManager
    private lateinit var request: DownloadManager.Request
    private var downloadId: Long = 0
@@ -103,33 +100,30 @@ class Downloader(private val applicationInfo: ApplicationInfo, private val fullD
        downloadManager.remove(downloadId)
    }

    @Throws(Exception::class)
    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 String(Hex.encodeHex(messageDigest.digest()))
    }

    private var onComplete: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            unregisterReceivers(context)
            val status = getDownloadStatus()
            if (status != null && status == DownloadManager.STATUS_SUCCESSFUL) {
                downloaderInterface.onDownloadComplete(context, DownloadManager.STATUS_SUCCESSFUL)
                IntegrityVerificationTask(
                        applicationInfo,
                        fullData,
                        this@Downloader)
                        .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, context)
            } else {
                downloaderInterface.onDownloadComplete(context, DownloadManager.STATUS_FAILED)
            }
        }
    }

    override fun onIntegrityVerified(context: Context, verificationSuccessful: Boolean) {
        if (verificationSuccessful) {
            downloaderInterface.onDownloadComplete(context, DownloadManager.STATUS_SUCCESSFUL)
        } else {
            downloaderInterface.onDownloadComplete(context, DownloadManager.STATUS_FAILED)
        }
    }

    private fun getDownloadStatus(): Int? {
        val query = DownloadManager.Query().apply {
            setFilterById(downloadId)
+101 −0
Original line number Diff line number Diff line
package foundation.e.apps.application.model

import android.content.Context
import android.os.AsyncTask
import foundation.e.apps.application.model.data.FullData
import org.apache.commons.codec.binary.Hex
import java.security.MessageDigest
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
import org.bouncycastle.openpgp.PGPUtil
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
import org.bouncycastle.openpgp.PGPSignatureList
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory
import org.bouncycastle.openpgp.PGPCompressedData
import java.io.*

class IntegrityVerificationTask(
        private val applicationInfo: ApplicationInfo,
        private val fullData: FullData,
        private val integrityVerificationCallback: IntegrityVerificationCallback) :
        AsyncTask<Context, Void, Context>() {
    private var verificationSuccessful: Boolean = false
    override fun doInBackground(vararg context: Context): Context {
        verificationSuccessful = if (!fullData.getLastVersion()!!.apkSHA.isNullOrEmpty()) {
            getApkFileSha1(applicationInfo.getApkFile(context[0], fullData.basicData)) ==
                    fullData.getLastVersion()!!.apkSHA
        } else {
            Security.addProvider(BouncyCastleProvider())
            verifyAPKSignature(
                    BufferedInputStream(FileInputStream(
                            applicationInfo.getApkFile(context[0],
                                    fullData.basicData).absolutePath)),
                    fullData.getLastVersion()!!.signature.byteInputStream(Charsets.UTF_8),
                    context[0].assets.open("f-droid.org-signing-key.gpg"))
        }
        return context[0]
    }

    override fun onPostExecute(context: Context) {
        integrityVerificationCallback.onIntegrityVerified(context, verificationSuccessful)
    }

    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 String(Hex.encodeHex(messageDigest.digest()))
    }

    private fun verifyAPKSignature(
            apkInputStream: BufferedInputStream,
            apkSignatureInputStream: InputStream,
            publicKeyInputStream: InputStream): Boolean {
        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(JcaPGPContentVerifierBuilderProvider().setProvider("BC"), key)

        var character = apkInputStream.read()
        while (character >= 0) {
            signature.update(character.toByte())
            character = apkInputStream.read()
        }

        apkInputStream.close()
        apkSignatureInputStream.close()
        publicKeyInputStream.close()

        return signature.verify()
    }
}

interface IntegrityVerificationCallback {
    fun onIntegrityVerified(context: Context, verificationSuccessful: Boolean)
}