IntegrityVerificationTask.kt 10.2 KB
Newer Older
1
/*
2
 * Copyright (C) 2019-2021  E FOUNDATION
3
 * Copyright (C) 2021 ECORP SAS
4
5
6
7
8
9
10
11
12
13
14
15
16
 *
 * 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 <https://www.gnu.org/licenses/>.
17
18
 */

19
20
21
package foundation.e.apps.application.model

import android.content.Context
22
23
24
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
25
import android.os.AsyncTask
narinder Rana's avatar
narinder Rana committed
26
27
import android.os.Handler
import android.os.Looper
28
import android.util.Log
narinder Rana's avatar
narinder Rana committed
29
30
import android.widget.Toast
import foundation.e.apps.R
31
import foundation.e.apps.api.FDroidAppExistsRequest
32
33
import foundation.e.apps.application.model.data.FullData
import org.bouncycastle.jce.provider.BouncyCastleProvider
Arnau Vàzquez's avatar
Arnau Vàzquez committed
34
import org.bouncycastle.openpgp.PGPCompressedData
35
36
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
import org.bouncycastle.openpgp.PGPSignatureList
Arnau Vàzquez's avatar
Arnau Vàzquez committed
37
import org.bouncycastle.openpgp.PGPUtil
38
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory
Arnau Vàzquez's avatar
Arnau Vàzquez committed
39
40
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
41
42
import org.json.JSONException
import org.json.JSONObject
narinder Rana's avatar
narinder Rana committed
43
import java.io.*
Arnau Vàzquez's avatar
Arnau Vàzquez committed
44
45
import java.security.MessageDigest
import java.security.Security
46
47

class IntegrityVerificationTask(
narinder Rana's avatar
narinder Rana committed
48
49
50
    private val applicationInfo: ApplicationInfo,
    private val fullData: FullData,
    private val integrityVerificationCallback: IntegrityVerificationCallback
Aayush Gupta's avatar
Aayush Gupta committed
51
) :
narinder Rana's avatar
narinder Rana committed
52

Aayush Gupta's avatar
Aayush Gupta committed
53
    AsyncTask<Context, Void, Context>() {
narinder Rana's avatar
narinder Rana committed
54
    private lateinit var systemJsonData: JSONObject
55
    private var verificationSuccessful: Boolean = false
56
57
    private var TAG = "IntegrityVerificationTask"

58
    override fun doInBackground(vararg context: Context): Context {
Aayush Gupta's avatar
Aayush Gupta committed
59
        try {
60
            val packageName = getAPK_PackageName(context[0])
narinder Rana's avatar
narinder Rana committed
61
            verificationSuccessful = if (isSystemApplication(context[0], packageName.toString())) {
62
                verifyAPKSignature(context[0])
narinder Rana's avatar
narinder Rana committed
63
            } else if (isfDroidApplication(packageName.toString())) {
Aayush Gupta's avatar
Aayush Gupta committed
64
65
66
67
68
69
70
                verifyFdroidSignature(context[0])
            } else {
                checkGoogleApp(context[0])
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
71
72
73
74
75
76
77
        return context[0]
    }

    private fun checkGoogleApp(context: Context): Boolean {
        return verifyShaSum(context)
    }

Aayush Gupta's avatar
Aayush Gupta committed
78
    private fun verifyShaSum(context: Context): Boolean {
79
80
        if (!fullData.getLastVersion()!!.apkSHA.isNullOrEmpty()) {
            return getApkFileSha1(applicationInfo.getApkOrXapkFile(context, fullData, fullData.basicData)) ==
Aayush Gupta's avatar
Aayush Gupta committed
81
                fullData.getLastVersion()!!.apkSHA
82
83
84
85
        }
        return false
    }

86
87
    private fun verifyAPKSignature(context: Context): Boolean {

narinder Rana's avatar
narinder Rana committed
88
        if (getAPKSignature(context) != null) {
89
            return getAPKSignature(context)?.toCharsString() ==
narinder Rana's avatar
narinder Rana committed
90
                getSystemSignature(context.packageManager)?.toCharsString()
91
92
93
        }
        return false
    }
narinder Rana's avatar
undo    
narinder Rana committed
94

95
96
97
    private fun getAPKSignature(context: Context): Signature? {
        try {
            val fullPath: String = applicationInfo.getApkFile(
narinder Rana's avatar
narinder Rana committed
98
99
                context,
                fullData.basicData
100
101
102
103
104
105
106
107
108
            ).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
    }
narinder Rana's avatar
undo    
narinder Rana committed
109

narinder Rana's avatar
narinder Rana committed
110
    private fun getAPK_PackageName(context: Context): String? {
narinder Rana's avatar
narinder Rana committed
111
112
113

        val pm: PackageManager = context.packageManager
        val fullPath: String = applicationInfo.getApkFile(
narinder Rana's avatar
narinder Rana committed
114
115
            context,
            fullData.basicData
narinder Rana's avatar
narinder Rana committed
116
117
118
        ).absolutePath
        val info = pm.getPackageArchiveInfo(fullPath, 0)
        if (info != null) {
narinder Rana's avatar
narinder Rana committed
119
120
121
            return info.packageName
        } else
            return null
narinder Rana's avatar
narinder Rana committed
122
    }
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139

    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
    }

Aayush Gupta's avatar
Aayush Gupta committed
140
    private fun verifyFdroidSignature(context: Context): Boolean {
141
142
        Security.addProvider(BouncyCastleProvider())
        return verifyAPKSignature(
narinder Rana's avatar
narinder Rana committed
143
144
145
146
147
148
149
150
151
152
153
            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")
Aayush Gupta's avatar
Aayush Gupta committed
154
        )
155
156
157
158
159
    }

    private fun isfDroidApplication(packageName: String): Boolean {
        var fDroidAppExistsResponse = 0
        FDroidAppExistsRequest(packageName)
Aayush Gupta's avatar
Aayush Gupta committed
160
161
162
163
164
            .request { applicationError, searchResult ->
                when (applicationError) {
                    null -> {
                        if (searchResult.size > 0) {
                            fDroidAppExistsResponse = searchResult[0]!!
165
166
                        }
                    }
Aayush Gupta's avatar
Aayush Gupta committed
167
168
169
                    else -> {
                        Log.e(TAG, applicationError.toString())
                    }
170
                }
Aayush Gupta's avatar
Aayush Gupta committed
171
            }
172
173
174
        return fDroidAppExistsResponse == 200
    }

narinder Rana's avatar
narinder Rana committed
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
    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)

194
        try {
narinder Rana's avatar
narinder Rana committed
195
196
            if (JSONObject(jsonResponse).has(packageName)) {
                systemJsonData = JSONObject(jsonResponse).getJSONObject(packageName)
narinder Rana's avatar
narinder Rana committed
197
198
                return true
            }
199
200
201
        } catch (e: Exception) {
            if (e is JSONException) {
                Log.d(TAG, "$packageName is not a system application")
202
            } else {
203
                e.printStackTrace()
204
            }
205
        }
206
        return false
207
208
209
210
    }

    override fun onPostExecute(context: Context) {
        integrityVerificationCallback.onIntegrityVerified(context, verificationSuccessful)
Aayush Gupta's avatar
Aayush Gupta committed
211
212
        if (!verificationSuccessful) {
            Toast.makeText(context, R.string.not_able_install, Toast.LENGTH_LONG).show()
213
        }
214
215
    }

216
    private fun getApkFileSha1(file: File): String? {
217
218
219
220
221
222
223
224
225
226
        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)
            }
        }
227
228
        return byteArrayToHex(messageDigest.digest())
    }
229

230
231
232
233
    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()
234
235
236
    }

    private fun verifyAPKSignature(
narinder Rana's avatar
narinder Rana committed
237
238
239
240
        context: Context,
        apkInputStream: BufferedInputStream,
        apkSignatureInputStream: InputStream,
        publicKeyInputStream: InputStream
Aayush Gupta's avatar
Aayush Gupta committed
241
    ): Boolean {
narinder Rana's avatar
narinder Rana committed
242
        try {
243

narinder Rana's avatar
narinder Rana committed
244
            var jcaPGPObjectFactory =
Aayush Gupta's avatar
Aayush Gupta committed
245
                JcaPGPObjectFactory(PGPUtil.getDecoderStream(apkSignatureInputStream))
narinder Rana's avatar
narinder Rana committed
246
            val pgpSignatureList: PGPSignatureList
247

narinder Rana's avatar
narinder Rana committed
248
249
250
251
252
253
254
            val pgpObject = jcaPGPObjectFactory.nextObject()
            if (pgpObject is PGPCompressedData) {
                jcaPGPObjectFactory = JcaPGPObjectFactory(pgpObject.dataStream)
                pgpSignatureList = jcaPGPObjectFactory.nextObject() as PGPSignatureList
            } else {
                pgpSignatureList = pgpObject as PGPSignatureList
            }
255

narinder Rana's avatar
narinder Rana committed
256
            val pgpPublicKeyRingCollection =
Aayush Gupta's avatar
Aayush Gupta committed
257
                PGPPublicKeyRingCollection(
narinder Rana's avatar
narinder Rana committed
258
259
                    PGPUtil.getDecoderStream(publicKeyInputStream),
                    JcaKeyFingerprintCalculator()
Aayush Gupta's avatar
Aayush Gupta committed
260
                )
261

narinder Rana's avatar
narinder Rana committed
262
263
            val signature = pgpSignatureList.get(0)
            val key = pgpPublicKeyRingCollection.getPublicKey(signature.keyID)
Arnau Vàzquez's avatar
Arnau Vàzquez committed
264

narinder Rana's avatar
narinder Rana committed
265
            signature.init(BcPGPContentVerifierBuilderProvider(), key)
266

narinder Rana's avatar
narinder Rana committed
267
268
269
270
271
272
            val buff = ByteArray(1024)
            var read = apkInputStream.read(buff)
            while (read != -1) {
                signature.update(buff, 0, read)
                read = apkInputStream.read(buff)
            }
Arnau Vàzquez's avatar
Arnau Vàzquez committed
273

narinder Rana's avatar
narinder Rana committed
274
275
276
277
278
279
            apkInputStream.close()
            apkSignatureInputStream.close()
            publicKeyInputStream.close()
            return signature.verify()
        } catch (e: Exception) {
            e.printStackTrace()
narinder Rana's avatar
narinder Rana committed
280
281

            Handler(Looper.getMainLooper()).post {
282
                Toast.makeText(context, context.resources.getString(R.string.Signature_verification_failed), Toast.LENGTH_LONG).show()
narinder Rana's avatar
narinder Rana committed
283
            }
narinder Rana's avatar
narinder Rana committed
284
        }
narinder Rana's avatar
narinder Rana committed
285

Aayush Gupta's avatar
Aayush Gupta committed
286
        return false
287
    }
288
}
289
290
291
292

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