1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.tracing.perfetto.security
18 
19 import android.content.Context
20 import android.os.Build
21 import androidx.annotation.RequiresApi
22 import java.io.File
23 import java.io.FileNotFoundException
24 import java.security.MessageDigest
25 
26 internal class SafeLibLoader(context: Context) {
27     private val approvedLocations = listOfNotNull(getCodeCacheDir(context), context.cacheDir)
28 
29     // TODO(235105064): consider moving off the main thread (I/O work)
30     fun loadLib(file: File, abiToSha256Map: Map<String, String>) {
31         // ensure the file is in an approved location (and if not, copy it over to one)
32         val safeLocationFile = copyToSafeLocation(file)
33 
34         // verify checksum of the file
35         verifyChecksum(safeLocationFile, findAbiAwareSha(abiToSha256Map))
36 
37         // load the library
38         System.load(safeLocationFile.absolutePath)
39     }
40 
41     /**
42      * Copies the file to a location where the app has exclusive write access. No-op if the file is
43      * already in such location.
44      */
45     private fun copyToSafeLocation(file: File): File {
46         if (!file.exists()) throw FileNotFoundException("Cannot locate library file: $file")
47         val isInApprovedLocation =
48             approvedLocations.any { approvedLocation -> file.isDescendantOf(approvedLocation) }
49         return if (isInApprovedLocation) file
50         else file.copyTo(approvedLocations.first().resolve(file.name), overwrite = true)
51     }
52 
53     private fun verifyChecksum(file: File, expectedSha: String) {
54         val actualSha = calcSha256Digest(file)
55         if (actualSha != expectedSha)
56             throw IncorrectChecksumException(
57                 "Invalid checksum for file: $file. Ensure you are using correct" +
58                     " version of the library and clear local caches."
59             )
60     }
61 
62     private fun findAbiAwareSha(abiToShaMap: Map<String, String>): String {
63         @Suppress("DEPRECATION")
64         val abi =
65             when {
66                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ->
67                     Build.SUPPORTED_ABIS.first()
68                 else -> Build.CPU_ABI
69             }
70         return abiToShaMap.getOrElse(abi) {
71             throw MissingChecksumException("Cannot locate checksum for ABI: $abi in $abiToShaMap")
72         }
73     }
74 
75     private fun calcSha256Digest(file: File): String {
76         val digest = MessageDigest.getInstance("SHA-256")
77 
78         val buffer = ByteArray(1024)
79         file.inputStream().buffered().use { s ->
80             while (true) {
81                 val readCount = s.read(buffer)
82                 if (readCount <= 0) break
83                 digest.update(buffer, 0, readCount)
84             }
85         }
86 
87         return digest.digest().joinToString("") { "%02x".format(it) }
88     }
89 
90     private fun File.isDescendantOf(ancestor: File) =
91         generateSequence(this.parentFile) { it.parentFile }.any { it == ancestor }
92 
93     private fun getCodeCacheDir(context: Context): File? =
94         if (Build.VERSION.SDK_INT >= 21) Impl21.getCodeCacheDir(context) else null
95 
96     @RequiresApi(21)
97     private object Impl21 {
98         fun getCodeCacheDir(context: Context): File? = context.codeCacheDir
99     }
100 }
101 
102 internal class MissingChecksumException(message: String) : NoSuchElementException(message)
103 
104 internal class IncorrectChecksumException(message: String) : SecurityException(message)
105