SafeLibLoader.kt
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.tracing.perfetto.security
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import java.io.File
import java.io.FileNotFoundException
import java.security.MessageDigest
internal class SafeLibLoader(context: Context) {
private val approvedLocations = listOfNotNull(context.cacheDir, getCodeCacheDir(context))
// TODO(235105064): consider moving off the main thread (I/O work)
fun loadLib(file: File, abiToSha256Map: Map<String, String>) {
// verify that file is in an approved location
if (!file.exists()) throw FileNotFoundException("Cannot locate library file: $file")
if (approvedLocations.none { approvedLocation -> file.isChildOf(approvedLocation) })
throw UnapprovedLocationException(
"File is located in a path that is not on the approved list of locations. " +
"Approved list: $approvedLocations."
)
// verify checksum of the file
val expected = findAbiAwareSha(abiToSha256Map)
val actualSha = calcSha256Digest(file)
if (actualSha != expected) throw IncorrectChecksumException(
"Invalid checksum for file: $file. Ensure you are using correct version" +
" of the library and clear local caches."
)
// load the library after performing all checks
System.load(file.absolutePath)
}
private fun findAbiAwareSha(abiToShaMap: Map<String, String>): String {
@Suppress("DEPRECATION")
val abi = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> Build.SUPPORTED_ABIS.first()
else -> Build.CPU_ABI
}
return abiToShaMap.getOrElse(abi) {
throw MissingChecksumException("Cannot locate checksum for ABI: $abi in $abiToShaMap")
}
}
private fun calcSha256Digest(file: File): String {
val digest = MessageDigest.getInstance("SHA-256")
val buffer = ByteArray(1024)
file.inputStream().buffered().use { s ->
while (true) {
val readCount = s.read(buffer)
if (readCount <= 0) break
digest.update(buffer, 0, readCount)
}
}
return digest.digest().joinToString("") { "%02x".format(it) }
}
private fun File.isChildOf(ancestor: File) =
generateSequence(this) { it.parentFile }.any { it == ancestor }
private fun getCodeCacheDir(context: Context): File? =
if (Build.VERSION.SDK_INT >= 21) Impl21.getCodeCacheDir(context)
else null
@RequiresApi(21)
private object Impl21 {
fun getCodeCacheDir(context: Context): File? = context.codeCacheDir
}
}
internal class MissingChecksumException(message: String) : NoSuchElementException(message)
internal class IncorrectChecksumException(message: String) : SecurityException(message)
internal class UnapprovedLocationException(message: String) : SecurityException(message)