IntegrityVerificationTask.kt 10.7 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
32
import foundation.e.apps.api.FDroidAppExistsRequest
import foundation.e.apps.api.SystemAppExistsRequest
33
34
import foundation.e.apps.application.model.data.FullData
import org.bouncycastle.jce.provider.BouncyCastleProvider
Arnau Vàzquez's avatar
Arnau Vàzquez committed
35
import org.bouncycastle.openpgp.PGPCompressedData
36
37
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
import org.bouncycastle.openpgp.PGPSignatureList
Arnau Vàzquez's avatar
Arnau Vàzquez committed
38
import org.bouncycastle.openpgp.PGPUtil
39
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory
Arnau Vàzquez's avatar
Arnau Vàzquez committed
40
41
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
42
43
import org.json.JSONException
import org.json.JSONObject
Arnau Vàzquez's avatar
Arnau Vàzquez committed
44
45
46
47
48
49
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.Security
50
51

class IntegrityVerificationTask(
narinder Rana's avatar
narinder Rana committed
52
53
54
    private val applicationInfo: ApplicationInfo,
    private val fullData: FullData,
    private val integrityVerificationCallback: IntegrityVerificationCallback
Aayush Gupta's avatar
Aayush Gupta committed
55
) :
narinder Rana's avatar
narinder Rana committed
56

Aayush Gupta's avatar
Aayush Gupta committed
57
    AsyncTask<Context, Void, Context>() {
narinder Rana's avatar
narinder Rana committed
58
    private lateinit var systemJsonData: JSONObject
59
    private var verificationSuccessful: Boolean = false
60
61
    private var TAG = "IntegrityVerificationTask"

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

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

Aayush Gupta's avatar
Aayush Gupta committed
82
    private fun verifyShaSum(context: Context): Boolean {
83
84
        if (!fullData.getLastVersion()!!.apkSHA.isNullOrEmpty()) {
            return getApkFileSha1(applicationInfo.getApkOrXapkFile(context, fullData, fullData.basicData)) ==
Aayush Gupta's avatar
Aayush Gupta committed
85
                fullData.getLastVersion()!!.apkSHA
86
87
88
89
90
91
92
        }
        return false
    }

    private fun verifySystemSignature(context: Context): Boolean {
        if (!fullData.getLastVersion()?.signature.isNullOrEmpty()) {
            return fullData.getLastVersion()?.signature ==
Aayush Gupta's avatar
Aayush Gupta committed
93
                getSystemSignature(context.packageManager)?.toCharsString()
94
95
96
        }
        return false
    }
narinder Rana's avatar
undo    
narinder Rana committed
97

narinder Rana's avatar
narinder Rana committed
98
    // get signature from apk and check
99
100
    private fun verifyAPKSignature(context: Context): Boolean {

narinder Rana's avatar
narinder Rana committed
101
102
        // get Signature from APK
        if (getAPKSignature(context) != null) {
103
            return getAPKSignature(context)?.toCharsString() ==
narinder Rana's avatar
narinder Rana committed
104
                getSystemSignature(context.packageManager)?.toCharsString()
105
106
107
        }
        return false
    }
narinder Rana's avatar
undo    
narinder Rana committed
108

109
110
111
    private fun getAPKSignature(context: Context): Signature? {
        try {
            val fullPath: String = applicationInfo.getApkFile(
narinder Rana's avatar
narinder Rana committed
112
113
                context,
                fullData.basicData
114
115
116
117
118
119
120
121
122
            ).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
123

narinder Rana's avatar
narinder Rana committed
124
    private fun getAPK_PackageName(context: Context): String? {
narinder Rana's avatar
narinder Rana committed
125
126
127

        val pm: PackageManager = context.packageManager
        val fullPath: String = applicationInfo.getApkFile(
narinder Rana's avatar
narinder Rana committed
128
129
            context,
            fullData.basicData
narinder Rana's avatar
narinder Rana committed
130
131
132
        ).absolutePath
        val info = pm.getPackageArchiveInfo(fullPath, 0)
        if (info != null) {
narinder Rana's avatar
narinder Rana committed
133
            return info.packageName;
narinder Rana's avatar
undo    
narinder Rana committed
134
        }
narinder Rana's avatar
narinder Rana committed
135
136
137
138
        else
            return null;


narinder Rana's avatar
narinder Rana committed
139
    }
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

    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
157
    private fun verifyFdroidSignature(context: Context): Boolean {
158
159
        Security.addProvider(BouncyCastleProvider())
        return verifyAPKSignature(
narinder Rana's avatar
narinder Rana committed
160
161
162
163
164
165
166
167
168
169
170
            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
171
        )
172
173
174
175
176
    }

    private fun isfDroidApplication(packageName: String): Boolean {
        var fDroidAppExistsResponse = 0
        FDroidAppExistsRequest(packageName)
Aayush Gupta's avatar
Aayush Gupta committed
177
178
179
180
181
            .request { applicationError, searchResult ->
                when (applicationError) {
                    null -> {
                        if (searchResult.size > 0) {
                            fDroidAppExistsResponse = searchResult[0]!!
182
183
                        }
                    }
Aayush Gupta's avatar
Aayush Gupta committed
184
185
186
                    else -> {
                        Log.e(TAG, applicationError.toString())
                    }
187
                }
Aayush Gupta's avatar
Aayush Gupta committed
188
            }
189
190
191
192
193
194
        return fDroidAppExistsResponse == 200
    }

    private fun isSystemApplication(packageName: String): Boolean {
        var jsonResponse = ""
        SystemAppExistsRequest(packageName)
Aayush Gupta's avatar
Aayush Gupta committed
195
196
197
198
199
            .request { applicationError, searchResult ->
                when (applicationError) {
                    null -> {
                        if (searchResult.size > 0) {
                            jsonResponse = searchResult[0].toString()
200
201
                        }
                    }
Aayush Gupta's avatar
Aayush Gupta committed
202
203
204
                    else -> {
                        Log.e(TAG, applicationError.toString())
                    }
205
                }
Aayush Gupta's avatar
Aayush Gupta committed
206
            }
207
        try {
narinder Rana's avatar
narinder Rana committed
208
209
            if (JSONObject(jsonResponse).has(packageName)) {
                systemJsonData = JSONObject(jsonResponse).getJSONObject(packageName)
narinder Rana's avatar
narinder Rana committed
210
                return true
narinder Rana's avatar
narinder Rana committed
211
            } else {
narinder Rana's avatar
narinder Rana committed
212
213
                return false
            }
narinder Rana's avatar
narinder Rana committed
214
            // return JSONObject(jsonResponse).has(packageName);
215
216
217
        } catch (e: Exception) {
            if (e is JSONException) {
                Log.d(TAG, "$packageName is not a system application")
218
            } else {
219
                e.printStackTrace()
220
            }
221
        }
222
        return false
223
224
225
226
    }

    override fun onPostExecute(context: Context) {
        integrityVerificationCallback.onIntegrityVerified(context, verificationSuccessful)
Aayush Gupta's avatar
Aayush Gupta committed
227
228
        if (!verificationSuccessful) {
            Toast.makeText(context, R.string.not_able_install, Toast.LENGTH_LONG).show()
229
        }
230
231
    }

232
    private fun getApkFileSha1(file: File): String? {
233
234
235
236
237
238
239
240
241
242
        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)
            }
        }
243
244
        return byteArrayToHex(messageDigest.digest())
    }
245

246
247
248
249
    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()
250
251
252
    }

    private fun verifyAPKSignature(
narinder Rana's avatar
narinder Rana committed
253
254
255
256
        context: Context,
        apkInputStream: BufferedInputStream,
        apkSignatureInputStream: InputStream,
        publicKeyInputStream: InputStream
Aayush Gupta's avatar
Aayush Gupta committed
257
    ): Boolean {
narinder Rana's avatar
narinder Rana committed
258
        try {
259

narinder Rana's avatar
narinder Rana committed
260
            var jcaPGPObjectFactory =
Aayush Gupta's avatar
Aayush Gupta committed
261
                JcaPGPObjectFactory(PGPUtil.getDecoderStream(apkSignatureInputStream))
narinder Rana's avatar
narinder Rana committed
262
            val pgpSignatureList: PGPSignatureList
263

narinder Rana's avatar
narinder Rana committed
264
265
266
267
268
269
270
            val pgpObject = jcaPGPObjectFactory.nextObject()
            if (pgpObject is PGPCompressedData) {
                jcaPGPObjectFactory = JcaPGPObjectFactory(pgpObject.dataStream)
                pgpSignatureList = jcaPGPObjectFactory.nextObject() as PGPSignatureList
            } else {
                pgpSignatureList = pgpObject as PGPSignatureList
            }
271

narinder Rana's avatar
narinder Rana committed
272
            val pgpPublicKeyRingCollection =
Aayush Gupta's avatar
Aayush Gupta committed
273
                PGPPublicKeyRingCollection(
narinder Rana's avatar
narinder Rana committed
274
275
                    PGPUtil.getDecoderStream(publicKeyInputStream),
                    JcaKeyFingerprintCalculator()
Aayush Gupta's avatar
Aayush Gupta committed
276
                )
277

narinder Rana's avatar
narinder Rana committed
278
279
            val signature = pgpSignatureList.get(0)
            val key = pgpPublicKeyRingCollection.getPublicKey(signature.keyID)
Arnau Vàzquez's avatar
Arnau Vàzquez committed
280

narinder Rana's avatar
narinder Rana committed
281
            signature.init(BcPGPContentVerifierBuilderProvider(), key)
282

narinder Rana's avatar
narinder Rana committed
283
284
285
286
287
288
            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
289

narinder Rana's avatar
narinder Rana committed
290
291
292
293
294
295
            apkInputStream.close()
            apkSignatureInputStream.close()
            publicKeyInputStream.close()
            return signature.verify()
        } catch (e: Exception) {
            e.printStackTrace()
narinder Rana's avatar
narinder Rana committed
296
297

            Handler(Looper.getMainLooper()).post {
298
                Toast.makeText(context, context.resources.getString(R.string.Signature_verification_failed), Toast.LENGTH_LONG).show()
narinder Rana's avatar
narinder Rana committed
299
            }
narinder Rana's avatar
narinder Rana committed
300
        }
narinder Rana's avatar
narinder Rana committed
301

Aayush Gupta's avatar
Aayush Gupta committed
302
        return false
303
    }
304
}
305
306
307
308

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