Magisk/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt

511 lines
18 KiB
Kotlin
Raw Normal View History

2020-01-13 15:01:46 +01:00
package com.topjohnwu.magisk.core.tasks
2019-07-29 13:05:54 +02:00
import android.net.Uri
2021-04-21 18:41:42 +02:00
import android.system.Os
2020-02-29 02:44:03 +01:00
import android.widget.Toast
2020-07-07 00:40:05 +02:00
import androidx.annotation.WorkerThread
2020-02-29 02:44:03 +01:00
import androidx.core.os.postDelayed
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
2020-02-29 02:44:03 +01:00
import com.topjohnwu.magisk.R
2021-01-29 14:15:22 +01:00
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
2021-04-18 13:46:11 +02:00
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.reboot
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo
2020-08-18 15:31:15 +02:00
import com.topjohnwu.magisk.utils.Utils
2020-07-07 00:40:05 +02:00
import com.topjohnwu.signing.SignBoot
2019-07-29 13:05:54 +02:00
import com.topjohnwu.superuser.Shell
2020-07-07 00:40:05 +02:00
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.NOPList
2020-02-29 02:44:03 +01:00
import com.topjohnwu.superuser.internal.UiThreadHandler
2020-07-07 00:40:05 +02:00
import com.topjohnwu.superuser.io.SuFile
2021-02-05 13:41:01 +01:00
import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
2020-07-07 07:30:21 +02:00
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
2020-10-06 09:40:57 +02:00
import net.jpountz.lz4.LZ4FrameInputStream
2020-07-07 00:40:05 +02:00
import org.kamranzafar.jtar.TarEntry
import org.kamranzafar.jtar.TarHeader
import org.kamranzafar.jtar.TarInputStream
import org.kamranzafar.jtar.TarOutputStream
import timber.log.Timber
2020-09-26 22:32:51 +02:00
import java.io.*
2020-10-06 09:40:57 +02:00
import java.nio.ByteBuffer
import java.security.SecureRandom
import java.util.*
import java.util.zip.ZipFile
2020-07-07 00:40:05 +02:00
2021-01-18 22:32:10 +01:00
abstract class MagiskInstallImpl protected constructor(
protected val console: MutableList<String> = NOPList.getInstance(),
private val logs: MutableList<String> = NOPList.getInstance()
2021-04-18 13:46:11 +02:00
) {
2020-07-07 00:40:05 +02:00
2021-01-30 20:51:15 +01:00
protected var installDir = File("xxx")
2021-01-25 12:02:43 +01:00
private lateinit var srcBoot: File
2020-07-07 00:40:05 +02:00
2021-02-07 10:54:08 +01:00
private val shell = Shell.getShell()
2021-04-18 13:46:11 +02:00
private val service get() = ServiceLocator.networkService
protected val context get() = ServiceLocator.deContext
private val useRootDir = shell.isRoot && Info.noDataExec
2021-01-18 13:25:26 +01:00
2020-07-07 00:40:05 +02:00
private fun findImage(): Boolean {
2021-01-25 12:02:43 +01:00
val bootPath = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
if (bootPath.isEmpty()) {
2020-07-07 00:40:05 +02:00
console.add("! Unable to detect target image")
return false
}
2021-01-25 12:02:43 +01:00
srcBoot = SuFile(bootPath)
console.add("- Target image: $bootPath")
2020-07-07 00:40:05 +02:00
return true
}
2021-01-29 14:15:22 +01:00
private fun findSecondary(): Boolean {
2020-07-07 00:40:05 +02:00
val slot = "echo \$SLOT".fsh()
val target = if (slot == "_a") "_b" else "_a"
console.add("- Target slot: $target")
2021-01-25 12:02:43 +01:00
val bootPath = arrayOf(
2020-07-07 00:40:05 +02:00
"SLOT=$target",
"find_boot_image",
"SLOT=$slot",
"echo \"\$BOOTIMAGE\"").fsh()
2021-01-25 12:02:43 +01:00
if (bootPath.isEmpty()) {
2020-07-07 00:40:05 +02:00
console.add("! Unable to detect target image")
return false
}
2021-01-25 12:02:43 +01:00
srcBoot = SuFile(bootPath)
console.add("- Target image: $bootPath")
2020-07-07 00:40:05 +02:00
return true
}
private fun extractFiles(): Boolean {
console.add("- Device platform: ${Const.CPU_ABI}")
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
2020-07-07 00:40:05 +02:00
2021-01-29 14:15:22 +01:00
installDir = File(context.filesDir.parent, "install")
installDir.deleteRecursively()
installDir.mkdirs()
2021-01-18 13:25:26 +01:00
2020-07-07 00:40:05 +02:00
try {
// Extract binaries
if (isRunningAsStub) {
val zf = ZipFile(DynAPK.current(context))
zf.entries().asSequence().filter {
!it.isDirectory && it.name.startsWith("lib/${Const.CPU_ABI_32}/")
}.forEach {
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
val name = n.substring(3, n.length - 3)
2021-01-29 14:15:22 +01:00
val dest = File(installDir, name)
zf.getInputStream(it).writeTo(dest)
}
} else {
val libs = Const.NATIVE_LIB_DIR.listFiles { _, name ->
name.startsWith("lib") && name.endsWith(".so")
} ?: emptyArray()
for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3)
2021-04-21 18:41:42 +02:00
Os.symlink(lib.path, "$installDir/$name")
2020-07-07 00:40:05 +02:00
}
}
// Extract scripts
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
2021-01-29 14:15:22 +01:00
val dest = File(installDir, script)
2021-01-23 13:26:00 +01:00
context.assets.open(script).writeTo(dest)
}
// Extract chromeos tools
2021-01-29 14:15:22 +01:00
File(installDir, "chromeos").mkdir()
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
val name = "chromeos/$file"
2021-01-29 14:15:22 +01:00
val dest = File(installDir, name)
2021-01-23 13:26:00 +01:00
context.assets.open(name).writeTo(dest)
}
} catch (e: Exception) {
console.add("! Unable to extract files")
2020-07-07 00:40:05 +02:00
Timber.e(e)
return false
}
if (useRootDir) {
// Move everything to tmpfs to workaround Samsung bullshit
SuFile(Const.TMPDIR).also {
arrayOf(
"rm -rf $it",
"mkdir -p $it",
"cp_readlink $installDir $it",
"rm -rf $installDir"
).sh()
installDir = it
}
}
2020-07-07 00:40:05 +02:00
return true
}
// Optimization for SuFile I/O streams to skip an internal trial and error
private fun installDirFile(name: String): File {
return if (useRootDir)
SuFile(installDir, name)
else
File(installDir, name)
}
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
src.copyTo(out)
}
2021-01-29 14:15:22 +01:00
private fun newTarEntry(name: String, size: Long): TarEntry {
2020-07-07 00:40:05 +02:00
console.add("-- Writing: $name")
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
}
@Throws(IOException::class)
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
2020-07-07 00:40:05 +02:00
console.add("- Processing tar file")
2020-09-26 22:32:51 +02:00
val tarOut = TarOutputStream(output)
2020-07-07 00:40:05 +02:00
TarInputStream(input).use { tarIn ->
lateinit var entry: TarEntry
2020-10-06 09:40:57 +02:00
2021-01-29 14:15:22 +01:00
fun decompressedStream(): InputStream {
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
return object : FilterInputStream(src) {
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
override fun close() { /* Never close src stream */ }
}
}
2020-10-06 09:40:57 +02:00
2020-07-07 00:40:05 +02:00
while (tarIn.nextEntry?.let { entry = it } != null) {
2021-02-14 02:15:04 +01:00
if (entry.name.startsWith("boot.img") ||
2020-09-26 22:32:51 +02:00
(Config.recovery && entry.name.contains("recovery.img"))) {
2020-10-06 09:40:57 +02:00
val name = entry.name.replace(".lz4", "")
2020-07-07 00:40:05 +02:00
console.add("-- Extracting: $name")
2020-10-06 09:40:57 +02:00
val extract = installDirFile(name)
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
2020-10-06 09:40:57 +02:00
} else if (entry.name.contains("vbmeta.img")) {
2021-01-29 14:15:22 +01:00
val rawData = decompressedStream().readBytes()
2020-10-06 09:40:57 +02:00
// Valid vbmeta.img should be at least 256 bytes
if (rawData.size < 256)
continue
2021-01-12 04:47:36 +01:00
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
2020-10-06 09:40:57 +02:00
console.add("-- Patching: vbmeta.img")
2021-01-12 04:47:36 +01:00
ByteBuffer.wrap(rawData).putInt(120, 3)
2021-01-29 14:15:22 +01:00
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
2020-10-06 09:40:57 +02:00
tarOut.write(rawData)
2020-07-07 00:40:05 +02:00
} else {
2020-10-06 09:40:57 +02:00
console.add("-- Copying: ${entry.name}")
2020-07-07 00:40:05 +02:00
tarOut.putNextEntry(entry)
2020-10-06 09:40:57 +02:00
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
2020-07-07 00:40:05 +02:00
}
}
2021-01-25 12:02:43 +01:00
}
val boot = installDirFile("boot.img")
val recovery = installDirFile("recovery.img")
2021-01-25 12:02:43 +01:00
if (Config.recovery && recovery.exists() && boot.exists()) {
2021-01-29 14:15:22 +01:00
// Install to recovery
2021-01-25 12:02:43 +01:00
srcBoot = recovery
2021-01-29 14:15:22 +01:00
// Repack boot image to prevent auto restore
2021-01-25 12:02:43 +01:00
arrayOf(
2021-01-29 14:15:22 +01:00
"cd $installDir",
2021-05-10 07:56:54 +02:00
"chmod -R 755 .",
2021-01-25 12:02:43 +01:00
"./magiskboot unpack boot.img",
"./magiskboot repack boot.img",
"cat new-boot.img > boot.img",
2021-01-25 12:02:43 +01:00
"./magiskboot cleanup",
"rm -f new-boot.img",
2021-01-29 14:15:22 +01:00
"cd /").sh()
SuFileInputStream.open(boot).use {
2021-01-29 14:15:22 +01:00
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
2021-01-25 12:02:43 +01:00
it.copyTo(tarOut)
}
boot.delete()
} else {
if (!boot.exists()) {
console.add("! No boot image found")
throw IOException()
2020-07-07 00:40:05 +02:00
}
2021-01-25 12:02:43 +01:00
srcBoot = boot
2020-07-07 00:40:05 +02:00
}
2020-09-26 22:32:51 +02:00
return tarOut
2020-07-07 00:40:05 +02:00
}
private fun handleFile(uri: Uri): Boolean {
2020-09-26 22:32:51 +02:00
val outStream: OutputStream
var outFile: MediaStoreUtils.UriFile? = null
2020-09-26 22:32:51 +02:00
// Process input file
2020-07-07 00:40:05 +02:00
try {
2020-09-26 22:32:51 +02:00
uri.inputStream().buffered().use { src ->
src.mark(500)
2020-07-07 00:40:05 +02:00
val magic = ByteArray(5)
2020-09-26 22:32:51 +02:00
if (src.skip(257) != 257L || src.read(magic) != magic.size) {
console.add("! Invalid input file")
2020-07-07 00:40:05 +02:00
return false
}
2020-09-26 22:32:51 +02:00
src.reset()
val alpha = "abcdefghijklmnopqrstuvwxyz"
val alphaNum = "$alpha${alpha.toUpperCase(Locale.ROOT)}0123456789"
val random = SecureRandom()
val filename = StringBuilder("magisk_patched-${BuildConfig.VERSION_CODE}_").run {
2021-01-25 12:02:43 +01:00
for (i in 1..5) {
append(alphaNum[random.nextInt(alphaNum.length)])
}
toString()
}
2020-09-26 22:32:51 +02:00
outStream = if (magic.contentEquals("ustar".toByteArray())) {
// tar file
outFile = MediaStoreUtils.getFile("$filename.tar", true)
processTar(src, outFile!!.uri.outputStream())
2020-07-07 00:40:05 +02:00
} else {
// raw image
srcBoot = installDirFile("boot.img")
2020-07-07 00:40:05 +02:00
console.add("- Copying image to cache")
src.cleanPump(SuFileOutputStream.open(srcBoot))
outFile = MediaStoreUtils.getFile("$filename.img", true)
outFile!!.uri.outputStream()
2020-07-07 00:40:05 +02:00
}
}
} catch (e: IOException) {
console.add("! Process error")
outFile?.delete()
2020-07-07 00:40:05 +02:00
Timber.e(e)
return false
}
2020-09-26 22:32:51 +02:00
// Patch file
if (!patchBoot()) {
outFile!!.delete()
2020-09-26 22:32:51 +02:00
return false
}
2020-09-26 22:32:51 +02:00
// Output file
try {
val newBoot = installDirFile("new-boot.img")
2020-09-26 22:32:51 +02:00
if (outStream is TarOutputStream) {
2021-01-25 12:02:43 +01:00
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
2020-09-26 22:32:51 +02:00
}
SuFileInputStream.open(newBoot).cleanPump(outStream)
newBoot.delete()
2020-09-26 22:32:51 +02:00
console.add("")
console.add("****************************")
console.add(" Output file is written to ")
console.add(" $outFile ")
console.add("****************************")
} catch (e: IOException) {
console.add("! Failed to output to $outFile")
outFile!!.delete()
2020-09-26 22:32:51 +02:00
Timber.e(e)
return false
}
// Fix up binaries
2021-01-29 14:15:22 +01:00
srcBoot.delete()
if (shell.isRoot) {
"fix_env $installDir".sh()
} else {
"cp_readlink $installDir".sh()
}
2020-07-07 00:40:05 +02:00
return true
}
private fun patchBoot(): Boolean {
var isSigned = false
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
try {
2021-02-05 13:41:01 +01:00
SuFileInputStream.open(srcBoot).use {
if (SignBoot.verifySignature(it, null)) {
isSigned = true
console.add("- Boot image is signed with AVB 1.0")
}
2020-07-07 00:40:05 +02:00
}
} catch (e: IOException) {
console.add("! Unable to check signature")
Timber.e(e)
return false
2020-07-07 00:40:05 +02:00
}
}
val newBoot = installDirFile("new-boot.img")
if (!useRootDir) {
// Create output files before hand
newBoot.createNewFile()
File(installDir, "stock_boot.img").createNewFile()
}
2021-02-07 10:54:08 +01:00
val cmds = arrayOf(
"cd $installDir",
2021-01-18 13:25:26 +01:00
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
"KEEPVERITY=${Config.keepVerity} " +
2021-02-07 10:54:08 +01:00
"RECOVERYMODE=${Config.recovery} " +
"sh boot_patch.sh $srcBoot")
2021-01-18 13:25:26 +01:00
2021-02-07 10:54:08 +01:00
if (!cmds.sh().isSuccess)
2020-07-07 00:40:05 +02:00
return false
2021-01-29 14:15:22 +01:00
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
2020-07-07 00:40:05 +02:00
if (isSigned) {
console.add("- Signing boot image with verity keys")
2021-01-29 14:15:22 +01:00
val signed = File.createTempFile("signed", ".img", context.cacheDir)
2020-07-07 00:40:05 +02:00
try {
val src = SuFileInputStream.open(newBoot).buffered()
val out = signed.outputStream().buffered()
withStreams(src, out) { _, _ ->
2021-02-05 13:41:01 +01:00
SignBoot.doSignature(null, null, src, out, "/boot")
2020-07-07 00:40:05 +02:00
}
} catch (e: IOException) {
console.add("! Unable to sign image")
Timber.e(e)
return false
}
job.add("cat $signed > $newBoot", "rm -f $signed")
2020-07-07 00:40:05 +02:00
}
job.exec()
return true
}
2021-01-29 14:15:22 +01:00
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
2020-07-07 00:40:05 +02:00
2020-07-10 13:19:18 +02:00
private suspend fun postOTA(): Boolean {
2020-07-07 00:40:05 +02:00
try {
2021-01-29 14:15:22 +01:00
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
service.fetchBootctl().byteStream().writeTo(bootctl)
"post_ota $bootctl".sh()
2020-07-07 00:40:05 +02:00
} catch (e: IOException) {
console.add("! Unable to download bootctl")
Timber.e(e)
return false
}
console.add("***************************************")
console.add(" Next reboot will boot to second slot!")
console.add("***************************************")
return true
}
2021-01-29 14:15:22 +01:00
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
2020-07-07 00:40:05 +02:00
2021-02-07 10:54:08 +01:00
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
2020-07-07 00:40:05 +02:00
2021-02-07 10:54:08 +01:00
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
2020-07-07 00:40:05 +02:00
protected suspend fun secondSlot() =
2021-02-07 10:54:08 +01:00
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
2020-07-07 00:40:05 +02:00
2021-02-07 10:54:08 +01:00
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
2021-01-29 14:15:22 +01:00
2021-02-07 10:54:08 +01:00
protected fun uninstall() = "run_uninstaller ${AssetHack.apk}".sh().isSuccess
2020-07-07 00:40:05 +02:00
@WorkerThread
2020-07-10 13:19:18 +02:00
protected abstract suspend fun operations(): Boolean
2020-07-07 00:40:05 +02:00
open suspend fun exec(): Boolean {
2021-02-24 11:50:55 +01:00
synchronized(Companion) {
if (haveActiveSession)
return false
haveActiveSession = true
}
val result = withContext(Dispatchers.IO) { operations() }
2021-02-24 11:50:55 +01:00
synchronized(Companion) {
haveActiveSession = false
}
return result
}
2021-02-24 11:50:55 +01:00
companion object {
private var haveActiveSession = false
}
2020-07-07 00:40:05 +02:00
}
2019-07-29 13:05:54 +02:00
2021-01-18 22:32:10 +01:00
abstract class MagiskInstaller(
2020-07-07 00:40:05 +02:00
console: MutableList<String>,
2020-07-07 07:30:21 +02:00
logs: MutableList<String>
) : MagiskInstallImpl(console, logs) {
2019-07-29 13:05:54 +02:00
2020-07-07 07:30:21 +02:00
override suspend fun exec(): Boolean {
val success = super.exec()
2020-02-29 02:44:03 +01:00
if (success) {
console.add("- All done!")
2019-07-29 13:05:54 +02:00
} else {
Shell.sh("rm -rf $installDir").submit()
2020-02-29 02:44:03 +01:00
console.add("! Installation failed")
2019-07-29 13:05:54 +02:00
}
2020-07-07 07:30:21 +02:00
return success
2019-07-29 13:05:54 +02:00
}
2020-02-29 02:44:03 +01:00
class Patch(
private val uri: Uri,
console: MutableList<String>,
2020-07-07 07:30:21 +02:00
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
2020-07-10 13:19:18 +02:00
override suspend fun operations() = doPatchFile(uri)
2019-07-29 13:05:54 +02:00
}
2020-02-29 02:44:03 +01:00
class SecondSlot(
console: MutableList<String>,
2020-07-07 07:30:21 +02:00
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
2020-07-10 13:19:18 +02:00
override suspend fun operations() = secondSlot()
2019-07-29 13:05:54 +02:00
}
2020-02-29 02:44:03 +01:00
class Direct(
console: MutableList<String>,
2020-07-07 07:30:21 +02:00
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
2020-07-10 13:19:18 +02:00
override suspend fun operations() = direct()
2019-07-29 13:05:54 +02:00
}
2021-01-18 22:32:10 +01:00
class Emulator(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
2021-01-18 22:32:10 +01:00
override suspend fun operations() = fixEnv()
}
class Uninstall(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstallImpl(console, logs) {
override suspend fun operations() = uninstall()
2021-01-18 22:32:10 +01:00
override suspend fun exec(): Boolean {
val success = super.exec()
if (success) {
UiThreadHandler.handler.postDelayed(3000) {
Shell.su("pm uninstall ${context.packageName}").exec()
}
2021-01-18 22:32:10 +01:00
}
return success
}
}
class FixEnv(private val callback: () -> Unit) : MagiskInstallImpl() {
override suspend fun operations() = fixEnv()
2020-02-29 02:44:03 +01:00
override suspend fun exec(): Boolean {
val success = super.exec()
callback()
Utils.toast(
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
Toast.LENGTH_LONG
)
if (success)
UiThreadHandler.handler.postDelayed(5000) { reboot() }
return success
}
2019-07-29 13:05:54 +02:00
}
}