From 716e8363ffb32552b71d6fb443734c99fa8e2b56 Mon Sep 17 00:00:00 2001 From: cfig Date: Sat, 9 May 2020 00:09:54 +0800 Subject: [PATCH] initial GKI images support - boot.img header v3 - vendor_boot.img --- README.md | 32 +- aosp/system/tools/mkbootimg/mkbootimg.py | 6 +- bbootimg/src/main/kotlin/Helper.kt | 10 +- bbootimg/src/main/kotlin/InfoTable.kt | 8 - bbootimg/src/main/kotlin/ParamConfig.kt | 13 - bbootimg/src/main/kotlin/Signer.kt | 86 ---- bbootimg/src/main/kotlin/UnifiedConfig.kt | 152 ------ bbootimg/src/main/kotlin/avb/Avb.kt | 27 +- .../src/main/kotlin/bootimg/BootImgHeader.kt | 309 ------------ .../src/main/kotlin/bootimg/BootImgInfo.kt | 130 ----- bbootimg/src/main/kotlin/bootimg/Common.kt | 251 ++++++++++ bbootimg/src/main/kotlin/bootimg/ImgInfo.kt | 38 -- bbootimg/src/main/kotlin/bootimg/Packer.kt | 172 ------- bbootimg/src/main/kotlin/bootimg/Parser.kt | 163 ------ bbootimg/src/main/kotlin/bootimg/Signer.kt | 87 ++++ .../main/kotlin/bootimg/v2/BootHeaderV2.kt | 162 ++++++ bbootimg/src/main/kotlin/bootimg/v2/BootV2.kt | 470 ++++++++++++++++++ .../main/kotlin/bootimg/v3/BootHeaderV3.kt | 81 +++ bbootimg/src/main/kotlin/bootimg/v3/BootV3.kt | 231 +++++++++ .../src/main/kotlin/bootimg/v3/VendorBoot.kt | 213 ++++++++ .../kotlin/bootimg/v3/VendorBootHeader.kt | 88 ++++ .../kotlin/kernel_util/KernelExtractor.kt | 18 +- .../src/main/kotlin/packable/BootImgParser.kt | 135 ++--- .../src/main/kotlin/packable/DtboParser.kt | 14 +- .../src/main/kotlin/packable/IPackable.kt | 7 +- .../main/kotlin/packable/PackableLauncher.kt | 15 +- .../src/main/kotlin/packable/VBMetaParser.kt | 4 +- .../main/kotlin/packable/VendorBootParser.kt | 36 ++ bbootimg/src/main/resources/general.cfg | 10 + build.gradle.kts | 3 + doc/layout.md | 77 ++- src/integrationTest/resources | 2 +- 32 files changed, 1860 insertions(+), 1190 deletions(-) delete mode 100644 bbootimg/src/main/kotlin/InfoTable.kt delete mode 100644 bbootimg/src/main/kotlin/ParamConfig.kt delete mode 100644 bbootimg/src/main/kotlin/Signer.kt delete mode 100644 bbootimg/src/main/kotlin/UnifiedConfig.kt delete mode 100644 bbootimg/src/main/kotlin/bootimg/BootImgHeader.kt delete mode 100644 bbootimg/src/main/kotlin/bootimg/BootImgInfo.kt create mode 100644 bbootimg/src/main/kotlin/bootimg/Common.kt delete mode 100644 bbootimg/src/main/kotlin/bootimg/ImgInfo.kt delete mode 100644 bbootimg/src/main/kotlin/bootimg/Packer.kt delete mode 100644 bbootimg/src/main/kotlin/bootimg/Parser.kt create mode 100644 bbootimg/src/main/kotlin/bootimg/Signer.kt create mode 100644 bbootimg/src/main/kotlin/bootimg/v2/BootHeaderV2.kt create mode 100644 bbootimg/src/main/kotlin/bootimg/v2/BootV2.kt create mode 100644 bbootimg/src/main/kotlin/bootimg/v3/BootHeaderV3.kt create mode 100644 bbootimg/src/main/kotlin/bootimg/v3/BootV3.kt create mode 100644 bbootimg/src/main/kotlin/bootimg/v3/VendorBoot.kt create mode 100644 bbootimg/src/main/kotlin/bootimg/v3/VendorBootHeader.kt create mode 100644 bbootimg/src/main/kotlin/packable/VendorBootParser.kt create mode 100644 bbootimg/src/main/resources/general.cfg diff --git a/README.md b/README.md index f04d34a..258a502 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,21 @@ This tool focuses on editing Android boot.img(also recovery.img, and vbmeta.img) ## 1. Prerequisite #### 1.1 Host OS requirement: -Linux or Mac. -Also need python 2.x and jdk 8. +Linux or Mac development env. To get the most of the toolkit, following packages are also needed: python, jdk 8+, zlib1g-dev, cpio, device-tree-compiler. #### 1.2 Target Android requirement: (1) Target boot.img MUST follows AOSP verified boot flow, either [Boot image signature](https://source.android.com/security/verifiedboot/verified-boot#signature_format) in VBoot 1.0 or [AVB HASH footer](https://android.googlesource.com/platform/external/avb/+/master/README.md#The-VBMeta-struct) (a.k.a. AVB) in VBoot 2.0. Supported images: - - boot.img - - recovery.img (also recovery-two-step.img) - - vbmeta.img (also vbmeta\_system.img, vbmeta\_vendor.img etc.) - - dtbo.img (only 'unpack' is supported) - - sparse images (system.img, vendor.img ...) + +| Image Type | typical file names | | +|-----------------|-------------------------------------|---| +| boot images | boot.img, vendor_boot.img | | +| recovery images | recovery.img, recovery-two-step.img | | +| vbmeta images | vbmeta.img, vbmeta_system.img etc. | | +| sparse images | system.img, vendor.img etc. | | +| dtbo images | dtbo.img | | (2) These utilities are known to work for Nexus/Pixel boot.img for the following Android releases: @@ -40,13 +42,13 @@ Put your boot.img to current directory, then start gradle 'unpack' task: Your get the flattened kernel and /root filesystem under **./build/unzip\_boot**: build/unzip_boot/ - ├── boot.img.avb.json (AVB only) - ├── bootimg.json (boot image info) + ├── boot.json (boot image info) + ├── boot.avb.json (AVB only) ├── kernel - ├── second (2nd bootloader, if exists) - ├── dtb (dtb, if exists) - ├── dtbo (dtbo, if exists) - └── root + ├── second (2nd bootloader, if exists) + ├── dtb (dtb, if exists) + ├── dtbo (dtbo, if exists) + └── root (extracted initramfs) Then you can edit the actual file contents, like rootfs or kernel. Now, pack the boot.img again @@ -76,8 +78,7 @@ And you get recovery.img.signed An example boot.img has been placed at **src/test/resources/boot.img**, which is extracted from Nexus 5x(code: bullhead) factory images from [Google](https://dl.google.com/dl/android/aosp/bullhead-mda89e-factory-29247942.tgz), you can take it as a quick start. ## 4. boot.img layout -Read [layout](doc/layout.md) of Android boot.img. -We now support both VB 1.0 and AVB 2.0 layouts. +Read [layout](doc/layout.md) of Android boot.img and vendor\_boot.img. ## 5. compatible devices @@ -99,6 +100,7 @@ https://android.googlesource.com/platform/system/extras cpio / fs\_config https://android.googlesource.com/platform/system/core +https://www.kernel.org/doc/Documentation/early-userspace/buffer-format.txt AVB https://android.googlesource.com/platform/external/avb/ diff --git a/aosp/system/tools/mkbootimg/mkbootimg.py b/aosp/system/tools/mkbootimg/mkbootimg.py index 4733107..00a4623 100755 --- a/aosp/system/tools/mkbootimg/mkbootimg.py +++ b/aosp/system/tools/mkbootimg/mkbootimg.py @@ -64,7 +64,7 @@ def get_recovery_dtbo_offset(args): def write_header_v3(args): - BOOT_IMAGE_HEADER_V3_SIZE = 1596 + BOOT_IMAGE_HEADER_V3_SIZE = 1580 BOOT_MAGIC = 'ANDROID!'.encode() args.output.write(pack('8s', BOOT_MAGIC)) @@ -82,7 +82,7 @@ def write_header_v3(args): pad_file(args.output, BOOT_IMAGE_HEADER_V3_PAGESIZE) def write_vendor_boot_header(args): - VENDOR_BOOT_IMAGE_HEADER_V3_SIZE = 2108 + VENDOR_BOOT_IMAGE_HEADER_V3_SIZE = 2112 BOOT_MAGIC = 'VNDRBOOT'.encode() args.vendor_boot.write(pack('8s', BOOT_MAGIC)) @@ -214,7 +214,7 @@ def parse_os_version(x): def parse_os_patch_level(x): - match = re.search(r'^(\d{4})-(\d{2})-(\d{2})', x) + match = re.search(r'^(\d{4})-(\d{2})(?:-(\d{2}))?', x) if match: y = int(match.group(1)) - 2000 m = int(match.group(2)) diff --git a/bbootimg/src/main/kotlin/Helper.kt b/bbootimg/src/main/kotlin/Helper.kt index 3c11046..c605aff 100644 --- a/bbootimg/src/main/kotlin/Helper.kt +++ b/bbootimg/src/main/kotlin/Helper.kt @@ -13,9 +13,9 @@ import org.slf4j.LoggerFactory import java.io.* import java.math.BigInteger import java.math.RoundingMode -import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Paths +import java.util.* import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream import javax.crypto.Cipher @@ -23,6 +23,14 @@ import javax.crypto.Cipher @OptIn(ExperimentalUnsignedTypes::class) class Helper { companion object { + private val gcfg: Properties = Properties().apply { + load(Helper::class.java.classLoader.getResourceAsStream("general.cfg")) + } + + fun prop(k: String): String { + return gcfg.getProperty(k) + } + fun joinWithNulls(vararg source: ByteArray?): ByteArray { val baos = ByteArrayOutputStream() for (src in source) { diff --git a/bbootimg/src/main/kotlin/InfoTable.kt b/bbootimg/src/main/kotlin/InfoTable.kt deleted file mode 100644 index 13cd268..0000000 --- a/bbootimg/src/main/kotlin/InfoTable.kt +++ /dev/null @@ -1,8 +0,0 @@ -package cfig - -import de.vandermeer.asciitable.AsciiTable - -object InfoTable { - val instance = AsciiTable() - val missingParts = mutableListOf() -} \ No newline at end of file diff --git a/bbootimg/src/main/kotlin/ParamConfig.kt b/bbootimg/src/main/kotlin/ParamConfig.kt deleted file mode 100644 index de3b30b..0000000 --- a/bbootimg/src/main/kotlin/ParamConfig.kt +++ /dev/null @@ -1,13 +0,0 @@ -package cfig - -@OptIn(ExperimentalUnsignedTypes::class) -data class ParamConfig( - //file input - var kernel: String = UnifiedConfig.workDir + "kernel", - var ramdisk: String? = UnifiedConfig.workDir + "ramdisk.img.gz", - var second: String? = UnifiedConfig.workDir + "second", - var dtbo: String? = UnifiedConfig.workDir + "recoveryDtbo", - var dtb: String? = UnifiedConfig.workDir + "dtb", - var cfg: String = UnifiedConfig.workDir + "bootimg.json", - val mkbootimg: String = "./aosp/system/tools/mkbootimg/mkbootimg.py") - diff --git a/bbootimg/src/main/kotlin/Signer.kt b/bbootimg/src/main/kotlin/Signer.kt deleted file mode 100644 index 45a9ff3..0000000 --- a/bbootimg/src/main/kotlin/Signer.kt +++ /dev/null @@ -1,86 +0,0 @@ -package cfig - -import avb.AVBInfo -import avb.alg.Algorithms -import cfig.Avb.Companion.getJsonFileName -import cfig.bootimg.BootImgInfo -import com.fasterxml.jackson.databind.ObjectMapper -import org.apache.commons.exec.CommandLine -import org.apache.commons.exec.DefaultExecutor -import org.slf4j.LoggerFactory -import java.io.File - -class Signer { - @OptIn(ExperimentalUnsignedTypes::class) - companion object { - private val log = LoggerFactory.getLogger(Signer::class.java) - - fun sign(avbtool: String, bootSigner: String) { - log.info("Loading config from ${ParamConfig().cfg}") - val info2 = UnifiedConfig.readBack2() - val cfg = ObjectMapper().readValue(File(ParamConfig().cfg), UnifiedConfig::class.java) - - when (info2.signatureType) { - BootImgInfo.VerifyType.VERIFY -> { - log.info("Signing with verified-boot 1.0 style") - val sig = ImgInfo.VeritySignature() - val bootSignCmd = "java -jar $bootSigner " + - "${sig.path} ${cfg.info.output}.clear " + - "${sig.verity_pk8} ${sig.verity_pem} " + - "${cfg.info.output}.signed" - log.info(bootSignCmd) - DefaultExecutor().execute(CommandLine.parse(bootSignCmd)) - } - BootImgInfo.VerifyType.AVB -> { - log.info("Adding hash_footer with verified-boot 2.0 style") - val ai = ObjectMapper().readValue(File(Avb.getJsonFileName(cfg.info.output)), AVBInfo::class.java) - val alg = Algorithms.get(ai.header!!.algorithm_type.toInt()) - val bootDesc = ai.auxBlob!!.hashDescriptors[0] - val newAvbInfo = ObjectMapper().readValue(File(getJsonFileName(cfg.info.output)), AVBInfo::class.java) - - //our signer - File(cfg.info.output + ".clear").copyTo(File(cfg.info.output + ".signed")) - Avb().addHashFooter(cfg.info.output + ".signed", - info2.imageSize, - partition_name = bootDesc.partition_name, - newAvbInfo = newAvbInfo) - //original signer - CommandLine.parse("$avbtool add_hash_footer").apply { - addArguments("--image ${cfg.info.output}.signed2") - addArguments("--partition_size ${info2.imageSize}") - addArguments("--salt ${Helper.toHexString(bootDesc.salt)}") - addArguments("--partition_name ${bootDesc.partition_name}") - addArguments("--hash_algorithm ${bootDesc.hash_algorithm}") - addArguments("--algorithm ${alg!!.name}") - if (alg.defaultKey.isNotBlank()) { - addArguments("--key ${alg.defaultKey}") - } - newAvbInfo.auxBlob?.let { newAuxblob -> - newAuxblob.propertyDescriptor.forEach { newProp -> - addArguments(arrayOf("--prop", "${newProp.key}:${newProp.value}")) - } - } - addArgument("--internal_release_string") - addArgument(ai.header!!.release_string, false) - log.info(this.toString()) - - File(cfg.info.output + ".clear").copyTo(File(cfg.info.output + ".signed2")) - DefaultExecutor().execute(this) - } - //TODO: decide what to verify - //Parser.verifyAVBIntegrity(cfg.info.output + ".signed", avbtool) - //Parser.verifyAVBIntegrity(cfg.info.output + ".signed2", avbtool) - } - } - } - - fun mapToJson(m: LinkedHashMap<*, *>): String { - val sb = StringBuilder() - m.forEach { k, v -> - if (sb.isNotEmpty()) sb.append(", ") - sb.append("\"$k\": \"$v\"") - } - return "{ $sb }" - } - } -} diff --git a/bbootimg/src/main/kotlin/UnifiedConfig.kt b/bbootimg/src/main/kotlin/UnifiedConfig.kt deleted file mode 100644 index 54ca8ec..0000000 --- a/bbootimg/src/main/kotlin/UnifiedConfig.kt +++ /dev/null @@ -1,152 +0,0 @@ -package cfig - -import cfig.bootimg.BootImgInfo -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper -import java.io.File - -@OptIn(ExperimentalUnsignedTypes::class) -@JsonInclude(JsonInclude.Include.NON_NULL) -data class UnifiedConfig( - var info: MiscInfo = MiscInfo(), - var kernel: CommArgs = CommArgs(), - var ramdisk: CommArgs? = null, - var secondBootloader: CommArgs? = null, - var recoveryDtbo: CommArgs? = null, - var dtb: CommArgs? = null, - var signature: Any? = null -) { - data class CommArgs( - var file: String? = null, - var position: String = "0", - var size: String = "0", - var loadOffset: String = "0") - - data class MiscInfo( - var output: String = "", - var headerVersion: UInt = 0U, - var headerSize: UInt = 0U, - var loadBase: String = "", - var tagsOffset: String = "0", - var board: String? = null, - var pageSize: UInt = 0U, - var cmdline: String = "", - var osVersion: String? = null, - var osPatchLevel: String? = null, - var hash: ByteArray = byteArrayOf(), - var verify: BootImgInfo.VerifyType = BootImgInfo.VerifyType.VERIFY, - var imageSize: Long = 0) - - fun toBootImgInfo(): BootImgInfo { - val ret = BootImgInfo() - ret.kernelOffset = this.kernel.loadOffset.removePrefix("0x").toUInt(16) - ret.kernelLength = Integer.decode(this.kernel.size).toUInt() - - ret.kernelOffset = this.kernel.loadOffset.removePrefix("0x").toUInt(16) - ret.kernelLength = Integer.decode(this.kernel.size).toUInt() - - this.ramdisk?.let { - ret.ramdiskOffset = it.loadOffset.removePrefix("0x").toUInt(16) - ret.ramdiskLength = it.size.removePrefix("0x").toUInt(16) - } - - this.secondBootloader?.let { - ret.secondBootloaderOffset = it.loadOffset.removePrefix("0x").toUInt(16) - ret.secondBootloaderLength = it.size.removePrefix("0x").toUInt(16) - } - - this.recoveryDtbo?.let { - ret.recoveryDtboOffset = it.loadOffset.removePrefix("0x").toULong(16) - ret.recoveryDtboLength = it.size.removePrefix("0x").toUInt(16) - } - - this.dtb?.let { - ret.dtbOffset = it.loadOffset.removePrefix("0x").toULong(16) - ret.dtbLength = it.size.removePrefix("0x").toUInt(16) - } - - ret.headerSize = this.info.headerSize - ret.headerVersion = this.info.headerVersion - this.info.board?.let { ret.board = it } - ret.tagsOffset = this.info.tagsOffset.removePrefix("0x").toUInt(16) - ret.cmdline = this.info.cmdline - ret.osVersion = this.info.osVersion - ret.osPatchLevel = this.info.osPatchLevel - ret.hash = this.info.hash - ret.pageSize = this.info.pageSize - ret.signatureType = this.info.verify - ret.imageSize = this.info.imageSize - - return ret - } - - companion object { - const val workDir = "build/unzip_boot/" - - fun fromBootImgInfo(info: BootImgInfo): UnifiedConfig { - val ret = UnifiedConfig() - val param = ParamConfig() - ret.kernel.file = param.kernel - ret.kernel.loadOffset = "0x${Integer.toHexString(info.kernelOffset.toInt())}" - ret.kernel.size = "0x${Integer.toHexString(info.kernelLength.toInt())}" - ret.kernel.position = "0x${Integer.toHexString(info.kernelPosition.toInt())}" - - ret.ramdisk = CommArgs() - ret.ramdisk!!.loadOffset = "0x${Integer.toHexString(info.ramdiskOffset.toInt())}" - ret.ramdisk!!.size = "0x${Integer.toHexString(info.ramdiskLength.toInt())}" - ret.ramdisk!!.position = "0x${java.lang.Long.toHexString(info.ramdiskPosition.toInt().toLong())}" - if (info.ramdiskLength > 0U) { - ret.ramdisk!!.file = param.ramdisk - } - - ret.secondBootloader = CommArgs() - ret.secondBootloader!!.loadOffset = "0x${Integer.toHexString(info.secondBootloaderOffset.toInt())}" - ret.secondBootloader!!.size = "0x${Integer.toHexString(info.secondBootloaderLength.toInt())}" - ret.secondBootloader!!.position = "0x${Integer.toHexString(info.secondBootloaderPosition.toInt())}" - if (info.secondBootloaderLength > 0U) { - ret.secondBootloader!!.file = param.second - } - - if (info.headerVersion > 0U) { - ret.recoveryDtbo = CommArgs() - if (info.recoveryDtboLength > 0U) { - ret.recoveryDtbo!!.file = param.dtbo - } - ret.recoveryDtbo!!.loadOffset = "0x${java.lang.Long.toHexString(info.recoveryDtboOffset.toLong())}" - ret.recoveryDtbo!!.size = "0x${Integer.toHexString(info.recoveryDtboLength.toInt())}" - ret.recoveryDtbo!!.position = "0x${java.lang.Long.toHexString(info.recoveryDtboPosition.toLong())}" - } - - if (info.headerVersion > 1U) { - ret.dtb = CommArgs() - if (info.dtbLength > 0U) { - ret.dtb!!.file = param.dtb - } - ret.dtb!!.loadOffset = "0x${Integer.toHexString(info.dtbOffset.toInt())}" - ret.dtb!!.size = "0x${Integer.toHexString(info.dtbLength.toInt())}" - ret.dtb!!.position = "0x${java.lang.Long.toHexString(info.dtbPosition.toLong())}" - } - - //ret.info.output = //unknown - ret.info.headerSize = info.headerSize - ret.info.headerVersion = info.headerVersion - ret.info.loadBase = "0x${java.lang.Long.toHexString(0)}" - ret.info.board = if (info.board.isBlank()) null else info.board - ret.info.tagsOffset = "0x${java.lang.Long.toHexString(info.tagsOffset.toLong())}" - ret.info.cmdline = info.cmdline - ret.info.osVersion = info.osVersion - ret.info.osPatchLevel = info.osPatchLevel - ret.info.hash = info.hash!! - ret.info.pageSize = info.pageSize - ret.info.verify = info.signatureType!! - ret.info.imageSize = info.imageSize - return ret - } - - fun readBack2(): BootImgInfo { - val param = ParamConfig() - return ObjectMapper().readValue(File(param.cfg), - UnifiedConfig::class.java).toBootImgInfo() - } - } -} diff --git a/bbootimg/src/main/kotlin/avb/Avb.kt b/bbootimg/src/main/kotlin/avb/Avb.kt index 0c6cd7a..4c54f4b 100644 --- a/bbootimg/src/main/kotlin/avb/Avb.kt +++ b/bbootimg/src/main/kotlin/avb/Avb.kt @@ -11,6 +11,8 @@ import cfig.Helper.Companion.paddingWith import cfig.io.Struct3 import com.fasterxml.jackson.databind.ObjectMapper import org.apache.commons.codec.binary.Hex +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.DefaultExecutor import org.slf4j.LoggerFactory import java.io.File import java.io.FileInputStream @@ -319,9 +321,28 @@ class Avb { const val AVB_VERSION_SUB = 0 fun getJsonFileName(image_file: String): String { - val fileName = File(image_file).name - val jsonFile = "$fileName.avb.json" - return UnifiedConfig.workDir + jsonFile + val jsonFile = File(image_file).name.removeSuffix(".img") + ".avb.json" + return Helper.prop("workDir") + jsonFile + } + + fun hasAvbFooter(fileName: String): Boolean { + val expectedBf = "AVBf".toByteArray() + FileInputStream(fileName).use { fis -> + fis.skip(File(fileName).length() - 64) + val bf = ByteArray(4) + fis.read(bf) + return bf.contentEquals(expectedBf) + } + } + + fun verifyAVBIntegrity(fileName: String, avbtool: String) { + val cmdline = "$avbtool verify_image --image $fileName" + log.info(cmdline) + try { + DefaultExecutor().execute(CommandLine.parse(cmdline)) + } catch (e: Exception) { + throw IllegalArgumentException("$fileName failed integrity check by \"$cmdline\"") + } } } } diff --git a/bbootimg/src/main/kotlin/bootimg/BootImgHeader.kt b/bbootimg/src/main/kotlin/bootimg/BootImgHeader.kt deleted file mode 100644 index 1dc8ee3..0000000 --- a/bbootimg/src/main/kotlin/bootimg/BootImgHeader.kt +++ /dev/null @@ -1,309 +0,0 @@ -package cfig.bootimg - -import cfig.Helper -import cfig.ParamConfig -import cfig.io.Struct3 -import org.slf4j.LoggerFactory -import java.io.File -import java.io.FileInputStream -import java.io.InputStream -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.security.MessageDigest -import java.util.regex.Pattern -import kotlin.math.pow - -@OptIn(ExperimentalUnsignedTypes::class) -open class BootImgHeader( - var kernelLength: UInt = 0U, - var kernelOffset: UInt = 0U, - - var ramdiskLength: UInt = 0U, - var ramdiskOffset: UInt = 0U, - - var secondBootloaderLength: UInt = 0U, - var secondBootloaderOffset: UInt = 0U, - - var recoveryDtboLength: UInt = 0U, - var recoveryDtboOffset: ULong = 0UL,//Q - - var dtbLength: UInt = 0U, - var dtbOffset: ULong = 0UL,//Q - - var tagsOffset: UInt = 0U, - - var pageSize: UInt = 0U, - - var headerSize: UInt = 0U, - var headerVersion: UInt = 0U, - - var board: String = "", - - var cmdline: String = "", - - var hash: ByteArray? = null, - - var osVersion: String? = null, - var osPatchLevel: String? = null) { - @Throws(IllegalArgumentException::class) - constructor(iS: InputStream?) : this() { - if (iS == null) { - return - } - log.warn("BootImgHeader constructor") - val info = Struct3(FORMAT_STRING).unpack(iS) - assert(20 == info.size) - if (info[0] != magic) { - throw IllegalArgumentException("stream doesn't look like Android Boot Image Header") - } - this.kernelLength = info[1] as UInt - this.kernelOffset = info[2] as UInt - this.ramdiskLength = info[3] as UInt - this.ramdiskOffset = info[4] as UInt - this.secondBootloaderLength = info[5] as UInt - this.secondBootloaderOffset = info[6] as UInt - this.tagsOffset = info[7] as UInt - this.pageSize = info[8] as UInt - this.headerVersion = info[9] as UInt - val osNPatch = info[10] as UInt - if (0U != osNPatch) { //treated as 'reserved' in this boot image - this.osVersion = parseOsVersion(osNPatch.toInt() shr 11) - this.osPatchLevel = parseOsPatchLevel((osNPatch and 0x7ff.toUInt()).toInt()) - } - this.board = info[11] as String - this.cmdline = (info[12] as String) + (info[14] as String) - this.hash = info[13] as ByteArray - - if (this.headerVersion > 0U) { - this.recoveryDtboLength = info[15] as UInt - this.recoveryDtboOffset = info[16] as ULong - } - - this.headerSize = info[17] as UInt - assert(this.headerSize.toInt() in intArrayOf(BOOT_IMAGE_HEADER_V2_SIZE, BOOT_IMAGE_HEADER_V1_SIZE)) - - if (this.headerVersion > 1U) { - this.dtbLength = info[18] as UInt - this.dtbOffset = info[19] as ULong - } - } - - private fun parseOsVersion(x: Int): String { - val a = x shr 14 - val b = x - (a shl 14) shr 7 - val c = x and 0x7f - return String.format("%d.%d.%d", a, b, c) - } - - private fun parseOsPatchLevel(x: Int): String { - var y = x shr 4 - val m = x and 0xf - y += 2000 - return String.format("%d-%02d-%02d", y, m, 0) - } - - @Throws(IllegalArgumentException::class) - private fun packOsVersion(x: String?): Int { - if (x.isNullOrBlank()) return 0 - val pattern = Pattern.compile("^(\\d{1,3})(?:\\.(\\d{1,3})(?:\\.(\\d{1,3}))?)?") - val m = pattern.matcher(x) - if (m.find()) { - val a = Integer.decode(m.group(1)) - var b = 0 - var c = 0 - if (m.groupCount() >= 2) { - b = Integer.decode(m.group(2)) - } - if (m.groupCount() == 3) { - c = Integer.decode(m.group(3)) - } - assert(a < 128) - assert(b < 128) - assert(c < 128) - return (a shl 14) or (b shl 7) or c - } else { - throw IllegalArgumentException("invalid os_version") - } - } - - private fun packOsPatchLevel(x: String?): Int { - if (x.isNullOrBlank()) return 0 - val ret: Int - val pattern = Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})") - val matcher = pattern.matcher(x) - if (matcher.find()) { - val y = Integer.parseInt(matcher.group(1), 10) - 2000 - val m = Integer.parseInt(matcher.group(2), 10) - // 7 bits allocated for the year, 4 bits for the month - assert(y in 0..127) - assert(m in 1..12) - ret = (y shl 4) or m - } else { - throw IllegalArgumentException("invalid os_patch_level") - } - - return ret - } - - @Throws(CloneNotSupportedException::class) - private fun hashFileAndSize(vararg inFiles: String?): ByteArray { - val md = MessageDigest.getInstance("SHA1") - for (item in inFiles) { - if (null == item) { - md.update(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) - .putInt(0) - .array()) - log.debug("update null $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) - } else { - val currentFile = File(item) - FileInputStream(currentFile).use { iS -> - var byteRead: Int - val dataRead = ByteArray(1024) - while (true) { - byteRead = iS.read(dataRead) - if (-1 == byteRead) { - break - } - md.update(dataRead, 0, byteRead) - } - log.debug("update file $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) - md.update(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) - .putInt(currentFile.length().toInt()) - .array()) - log.debug("update SIZE $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) - } - } - } - - return md.digest() - } - - private fun get_recovery_dtbo_offset(): UInt { - return Helper.round_to_multiple(this.headerSize, pageSize) + - Helper.round_to_multiple(this.kernelLength, pageSize) + - Helper.round_to_multiple(this.ramdiskLength, pageSize) + - Helper.round_to_multiple(this.secondBootloaderLength, pageSize) - } - - private fun refresh() { - val param = ParamConfig() - //refresh kernel size - if (0U == this.kernelLength) { - throw java.lang.IllegalArgumentException("kernel size can not be 0") - } else { - this.kernelLength = File(param.kernel).length().toUInt() - } - //refresh ramdisk size - if (0U == this.ramdiskLength) { - param.ramdisk = null - this.ramdiskOffset = 0U - } else { - this.ramdiskLength = File(param.ramdisk!!).length().toUInt() - } - //refresh second bootloader size - if (0U == this.secondBootloaderLength) { - param.second = null - this.secondBootloaderOffset = 0U - } else { - this.secondBootloaderLength = File(param.second!!).length().toUInt() - } - //refresh recovery dtbo size - if (0U == this.recoveryDtboLength) { - param.dtbo = null - this.recoveryDtboOffset = 0U - } else { - this.recoveryDtboLength = File(param.dtbo!!).length().toUInt() - this.recoveryDtboOffset = get_recovery_dtbo_offset().toULong() - log.warn("using fake recoveryDtboOffset $recoveryDtboOffset (as is in AOSP avbtool)") - } - //refresh dtb size - if (0U == this.dtbLength) { - param.dtb = null - } else { - this.dtbLength = File(param.dtb!!).length().toUInt() - } - //refresh image hash - val imageId = when (this.headerVersion) { - 0U -> { - hashFileAndSize(param.kernel, param.ramdisk, param.second) - } - 1U -> { - hashFileAndSize(param.kernel, param.ramdisk, param.second, param.dtbo) - } - 2U -> { - hashFileAndSize(param.kernel, param.ramdisk, param.second, param.dtbo, param.dtb) - } - else -> { - throw java.lang.IllegalArgumentException("headerVersion ${this.headerVersion} illegal") - } - } - this.hash = imageId - } - - fun encode(): ByteArray { - this.refresh() - val pageSizeChoices: MutableSet = mutableSetOf().apply { - (11..14).forEach { add(2.0.pow(it).toLong()) } - } - assert(pageSizeChoices.contains(pageSize.toLong())) { "invalid parameter [pageSize=$pageSize], (choose from $pageSizeChoices)" } - return Struct3(FORMAT_STRING).pack( - "ANDROID!", - //10I - kernelLength, - kernelOffset, - ramdiskLength, - ramdiskOffset, - secondBootloaderLength, - secondBootloaderOffset, - tagsOffset, - pageSize, - headerVersion, - (packOsVersion(osVersion) shl 11) or packOsPatchLevel(osPatchLevel), - //16s - board, - //512s - cmdline.substring(0, minOf(512, cmdline.length)), - //32b - hash!!, - //1024s - if (cmdline.length > 512) cmdline.substring(512) else "", - //I - recoveryDtboLength, - //Q - if (headerVersion > 0U) recoveryDtboOffset else 0, - //I - when (headerVersion) { - 0U -> 0 - 1U -> BOOT_IMAGE_HEADER_V1_SIZE - 2U -> BOOT_IMAGE_HEADER_V2_SIZE - else -> java.lang.IllegalArgumentException("headerVersion $headerVersion illegal") - }, - //I - dtbLength, - //Q - if (headerVersion > 1U) dtbOffset else 0 - ) - } - - companion object { - internal val log = LoggerFactory.getLogger(BootImgInfo::class.java) - const val magic = "ANDROID!" - const val FORMAT_STRING = "8s" + //"ANDROID!" - "10I" + - "16s" + //board name - "512s" + //cmdline part 1 - "32b" + //hash digest - "1024s" + //cmdline part 2 - "I" + //dtbo length [v1] - "Q" + //dtbo offset [v1] - "I" + //header size [v1] - "I" + //dtb length [v2] - "Q" //dtb offset [v2] - const val BOOT_IMAGE_HEADER_V2_SIZE = 1660 - const val BOOT_IMAGE_HEADER_V1_SIZE = 1648 - - init { - assert(BOOT_IMAGE_HEADER_V2_SIZE == Struct3(FORMAT_STRING).calcSize()) - } - } -} diff --git a/bbootimg/src/main/kotlin/bootimg/BootImgInfo.kt b/bbootimg/src/main/kotlin/bootimg/BootImgInfo.kt deleted file mode 100644 index d2a636c..0000000 --- a/bbootimg/src/main/kotlin/bootimg/BootImgInfo.kt +++ /dev/null @@ -1,130 +0,0 @@ -package cfig.bootimg - -import cfig.ParamConfig -import org.apache.commons.exec.CommandLine -import java.io.InputStream - -@OptIn(ExperimentalUnsignedTypes::class) -class BootImgInfo(iS: InputStream?) : BootImgHeader(iS) { - constructor() : this(null) - - val kernelPosition: UInt - get() { - return getHeaderSize(this.pageSize) - } - - val ramdiskPosition: UInt - get() { - return (kernelPosition + this.kernelLength + - getPaddingSize(this.kernelLength, this.pageSize)) - } - - val secondBootloaderPosition: UInt - get() { - return ramdiskPosition + ramdiskLength + - getPaddingSize(ramdiskLength, pageSize) - } - - val recoveryDtboPosition: ULong - get() { - return secondBootloaderPosition.toULong() + secondBootloaderLength + - getPaddingSize(secondBootloaderLength, pageSize) - } - - val dtbPosition: ULong - get() { - return recoveryDtboPosition + recoveryDtboLength + - getPaddingSize(recoveryDtboLength, pageSize) - } - - var signatureType: BootImgInfo.VerifyType? = null - - var imageSize: Long = 0 - - private fun getHeaderSize(pageSize: Int): Int { - val pad = (pageSize - (1648 and (pageSize - 1))) and (pageSize - 1) - return pad + 1648 - } - - private fun getHeaderSize(pageSize: UInt): UInt { - val pad = (pageSize - (1648U and (pageSize - 1U))) and (pageSize - 1U) - return pad + 1648U - } - - private fun getPaddingSize(position: UInt, pageSize: UInt): UInt { - return (pageSize - (position and pageSize - 1U)) and (pageSize - 1U) - } - - private fun getPaddingSize(position: Int, pageSize: Int): Int { - return (pageSize - (position and pageSize - 1)) and (pageSize - 1) - } - - fun toCommandLine(): CommandLine { - val param = ParamConfig() - val ret = CommandLine(param.mkbootimg) - ret.addArgument(" --header_version ") - ret.addArgument(headerVersion.toString()) - ret.addArgument(" --base ") - ret.addArgument("0x" + java.lang.Long.toHexString(0)) - ret.addArgument(" --kernel ") - ret.addArgument(param.kernel) - ret.addArgument(" --kernel_offset ") - ret.addArgument("0x" + Integer.toHexString(kernelOffset.toInt())) - if (this.ramdiskLength > 0U) { - ret.addArgument(" --ramdisk ") - ret.addArgument(param.ramdisk) - } - ret.addArgument(" --ramdisk_offset ") - ret.addArgument("0x" + Integer.toHexString(ramdiskOffset.toInt())) - if (this.secondBootloaderLength > 0U) { - ret.addArgument(" --second ") - ret.addArgument(param.second) - ret.addArgument(" --second_offset ") - ret.addArgument("0x" + Integer.toHexString(this.secondBootloaderOffset.toInt())) - } - if (!board.isBlank()) { - ret.addArgument(" --board ") - ret.addArgument(board) - } - if (headerVersion > 0U) { - if (this.recoveryDtboLength > 0U) { - ret.addArgument(" --recovery_dtbo ") - ret.addArgument(param.dtbo) - } - } - if (headerVersion > 1U) { - if (this.dtbLength > 0U) { - ret.addArgument("--dtb ") - ret.addArgument(param.dtb) - } - ret.addArgument("--dtb_offset ") - ret.addArgument("0x" + java.lang.Long.toHexString(this.dtbOffset.toLong())) - } - ret.addArgument(" --pagesize ") - ret.addArgument(Integer.toString(pageSize.toInt())) - ret.addArgument(" --cmdline ") - ret.addArgument(cmdline, false) - if (!osVersion.isNullOrBlank()) { - ret.addArgument(" --os_version ") - ret.addArgument(osVersion) - } - if (!osPatchLevel.isNullOrBlank()) { - ret.addArgument(" --os_patch_level ") - ret.addArgument(osPatchLevel) - } - ret.addArgument(" --tags_offset ") - ret.addArgument("0x" + Integer.toHexString(tagsOffset.toInt())) - ret.addArgument(" --id ") - ret.addArgument(" --output ") - //ret.addArgument("boot.img" + ".google") - - log.debug("To Commandline: " + ret.toString()) - - return ret - } - - enum class VerifyType { - VERIFY, - AVB - } -} diff --git a/bbootimg/src/main/kotlin/bootimg/Common.kt b/bbootimg/src/main/kotlin/bootimg/Common.kt new file mode 100644 index 0000000..b78e5c8 --- /dev/null +++ b/bbootimg/src/main/kotlin/bootimg/Common.kt @@ -0,0 +1,251 @@ +package cfig.bootimg + +import cfig.EnvironmentVerifier +import cfig.Helper +import cfig.dtb_util.DTC +import cfig.io.Struct3.InputStreamExt.Companion.getInt +import cfig.kernel_util.KernelExtractor +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.DefaultExecutor +import org.apache.commons.exec.PumpStreamHandler +import org.slf4j.LoggerFactory +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.MessageDigest +import java.util.regex.Pattern + +@OptIn(ExperimentalUnsignedTypes::class) +class Common { + data class VeritySignature( + var type: String = "dm-verity", + var path: String = "/boot", + var verity_pk8: String = "", + var verity_pem: String = "", + var jarPath: String = "") + + data class Slice( + var srcFile: String, + var offset: Int, + var length: Int, + var dumpFile: String + ) + + companion object { + private val log = LoggerFactory.getLogger(Common::class.java) + + @Throws(IllegalArgumentException::class) + fun packOsVersion(x: String?): Int { + if (x.isNullOrBlank()) return 0 + val pattern = Pattern.compile("^(\\d{1,3})(?:\\.(\\d{1,3})(?:\\.(\\d{1,3}))?)?") + val m = pattern.matcher(x) + if (m.find()) { + val a = Integer.decode(m.group(1)) + var b = 0 + var c = 0 + if (m.groupCount() >= 2) { + b = Integer.decode(m.group(2)) + } + if (m.groupCount() == 3) { + c = Integer.decode(m.group(3)) + } + assert(a < 128) + assert(b < 128) + assert(c < 128) + return (a shl 14) or (b shl 7) or c + } else { + throw IllegalArgumentException("invalid os_version") + } + } + + fun parseOsVersion(x: Int): String { + val a = x shr 14 + val b = x - (a shl 14) shr 7 + val c = x and 0x7f + return String.format("%d.%d.%d", a, b, c) + } + + fun packOsPatchLevel(x: String?): Int { + if (x.isNullOrBlank()) return 0 + val ret: Int + val pattern = Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})") + val matcher = pattern.matcher(x) + if (matcher.find()) { + val y = Integer.parseInt(matcher.group(1), 10) - 2000 + val m = Integer.parseInt(matcher.group(2), 10) + // 7 bits allocated for the year, 4 bits for the month + assert(y in 0..127) + assert(m in 1..12) + ret = (y shl 4) or m + } else { + throw IllegalArgumentException("invalid os_patch_level") + } + + return ret + } + + fun parseOsPatchLevel(x: Int): String { + var y = x shr 4 + val m = x and 0xf + y += 2000 + return String.format("%d-%02d-%02d", y, m, 0) + } + + fun parseKernelInfo(kernelFile: String): List { + KernelExtractor().let { ke -> + if (ke.envCheck()) { + return ke.run(kernelFile, File(".")) + } + } + return listOf() + } + + fun dumpKernel(s: Slice) { + Helper.extractFile(s.srcFile, s.dumpFile, s.offset.toLong(), s.length) + parseKernelInfo(s.dumpFile) + } + + fun dumpRamdisk(s: Slice, root: String) { + Helper.extractFile(s.srcFile, s.dumpFile, s.offset.toLong(), s.length) + Helper.unGnuzipFile(s.dumpFile, s.dumpFile.removeSuffix(".gz")) + unpackRamdisk(s.dumpFile.removeSuffix(".gz"), root) + } + + fun dumpDtb(s: Slice) { + Helper.extractFile(s.srcFile, s.dumpFile, s.offset.toLong(), s.length) + //extract DTB + if (EnvironmentVerifier().hasDtc) { + DTC().decompile(s.dumpFile, s.dumpFile + ".src") + } + } + + fun getPaddingSize(position: UInt, pageSize: UInt): UInt { + return (pageSize - (position and pageSize - 1U)) and (pageSize - 1U) + } + + fun getPaddingSize(position: Int, pageSize: Int): Int { + return (pageSize - (position and pageSize - 1)) and (pageSize - 1) + } + + @Throws(CloneNotSupportedException::class) + fun hashFileAndSize(vararg inFiles: String?): ByteArray { + val md = MessageDigest.getInstance("SHA1") + for (item in inFiles) { + if (null == item) { + md.update(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) + .putInt(0) + .array()) + log.debug("update null $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) + } else { + val currentFile = File(item) + FileInputStream(currentFile).use { iS -> + var byteRead: Int + val dataRead = ByteArray(1024) + while (true) { + byteRead = iS.read(dataRead) + if (-1 == byteRead) { + break + } + md.update(dataRead, 0, byteRead) + } + log.debug("update file $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) + md.update(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) + .putInt(currentFile.length().toInt()) + .array()) + log.debug("update SIZE $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) + } + } + } + + return md.digest() + } + + fun assertFileEquals(file1: String, file2: String) { + val hash1 = hashFileAndSize(file1) + val hash2 = hashFileAndSize(file2) + log.info("$file1 hash ${Helper.toHexString(hash1)}, $file2 hash ${Helper.toHexString(hash2)}") + if (hash1.contentEquals(hash2)) { + log.info("Hash verification passed: ${Helper.toHexString(hash1)}") + } else { + log.error("Hash verification failed") + throw UnknownError("Do not know why hash verification fails, maybe a bug") + } + } + + fun packRootfs(rootDir: String, ramdiskGz: String) { + val mkbootfs = Helper.prop("mkbootfsBin") + log.info("Packing rootfs $rootDir ...") + val outputStream = ByteArrayOutputStream() + DefaultExecutor().let { exec -> + exec.streamHandler = PumpStreamHandler(outputStream) + val cmdline = "$mkbootfs $rootDir" + log.info(cmdline) + exec.execute(CommandLine.parse(cmdline)) + } + Helper.gnuZipFile2(ramdiskGz, ByteArrayInputStream(outputStream.toByteArray())) + log.info("$ramdiskGz is ready") + } + + fun padFile(inBF: ByteBuffer, padding: Int) { + val pad = padding - (inBF.position() and padding - 1) and padding - 1 + inBF.put(ByteArray(pad)) + } + + fun File.deleleIfExists() { + if (this.exists()) { + if (!this.isFile) { + throw IllegalStateException("${this.canonicalPath} should be regular file") + } + log.info("Deleting ${this.path} ...") + this.delete() + } + } + + fun writePaddedFile(inBF: ByteBuffer, srcFile: String, padding: UInt) { + log.info("adding $srcFile into buffer ...") + assert(padding < Int.MAX_VALUE.toUInt()) + writePaddedFile(inBF, srcFile, padding.toInt()) + } + + fun writePaddedFile(inBF: ByteBuffer, srcFile: String, padding: Int) { + FileInputStream(srcFile).use { iS -> + var byteRead: Int + val dataRead = ByteArray(128) + while (true) { + byteRead = iS.read(dataRead) + if (-1 == byteRead) { + break + } + inBF.put(dataRead, 0, byteRead) + } + padFile(inBF, padding) + } + } + + fun unpackRamdisk(ramdisk: String, root: String) { + val rootFile = File(root).apply { + if (exists()) { + log.info("Cleaning [$root] before ramdisk unpacking") + deleteRecursively() + } + mkdirs() + } + + DefaultExecutor().let { exe -> + exe.workingDirectory = rootFile + exe.execute(CommandLine.parse("cpio -i -m -F " + File(ramdisk).canonicalPath)) + log.info(" ramdisk extracted : $ramdisk -> ${rootFile}") + } + } + + fun probeHeaderVersion(fileName: String): Int { + return FileInputStream(fileName).let { fis -> + fis.skip(40) + fis.getInt(ByteOrder.LITTLE_ENDIAN) + } + } + } +} diff --git a/bbootimg/src/main/kotlin/bootimg/ImgInfo.kt b/bbootimg/src/main/kotlin/bootimg/ImgInfo.kt deleted file mode 100644 index 578bfd6..0000000 --- a/bbootimg/src/main/kotlin/bootimg/ImgInfo.kt +++ /dev/null @@ -1,38 +0,0 @@ -package cfig - -data class ImgInfo( - //kernel - var kernelPosition: Int = 0, - var kernelLength: Int = 0, - //ramdisk - var ramdiskPosition: Int = 0, - var ramdiskLength: Int = 0, - //second bootloader - var secondBootloaderPosition: Int = 0, - var secondBootloaderLength: Int = 0, - //dtbo - var recoveryDtboPosition: Int = 0, - var recoveryDtboLength: Int = 0, - - var headerSize: Int = 0, - var hash: ByteArray = ByteArray(0), - - //signature - var signature: Any? = null -) { - data class AvbSignature( - var type: String = "avb", - var originalImageSize: Int? = null, - var imageSize: Int? = null, - var partName: String? = null, - var salt: String = "", - var hashAlgorithm: String? = null, - var algorithm: String? = null) - - data class VeritySignature( - var type: String = "dm-verity", - var path: String = "/boot", - var verity_pk8: String = "aosp/security/verity.pk8", - var verity_pem: String = "aosp/security/verity.x509.pem", - var jarPath: String = "aosp/boot_signer/build/libs/boot_signer.jar") -} diff --git a/bbootimg/src/main/kotlin/bootimg/Packer.kt b/bbootimg/src/main/kotlin/bootimg/Packer.kt deleted file mode 100644 index a8bb154..0000000 --- a/bbootimg/src/main/kotlin/bootimg/Packer.kt +++ /dev/null @@ -1,172 +0,0 @@ -package cfig - -import cfig.bootimg.BootImgInfo -import com.fasterxml.jackson.databind.ObjectMapper -import org.apache.commons.exec.CommandLine -import org.apache.commons.exec.DefaultExecutor -import org.apache.commons.exec.PumpStreamHandler -import org.slf4j.LoggerFactory -import java.io.* -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.security.MessageDigest - -@OptIn(ExperimentalUnsignedTypes::class) -class Packer { - private val log = LoggerFactory.getLogger("Packer") - - @Throws(CloneNotSupportedException::class) - private fun hashFileAndSize(vararg inFiles: String?): ByteArray { - val md = MessageDigest.getInstance("SHA1") - for (item in inFiles) { - if (null == item) { - md.update(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) - .putInt(0) - .array()) - log.debug("update null $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) - } else { - val currentFile = File(item) - FileInputStream(currentFile).use { iS -> - var byteRead: Int - val dataRead = ByteArray(1024) - while (true) { - byteRead = iS.read(dataRead) - if (-1 == byteRead) { - break - } - md.update(dataRead, 0, byteRead) - } - log.debug("update file $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) - md.update(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN) - .putInt(currentFile.length().toInt()) - .array()) - log.debug("update SIZE $item: " + Helper.toHexString((md.clone() as MessageDigest).digest())) - } - } - } - - return md.digest() - } - - private fun writePaddedFile(inBF: ByteBuffer, srcFile: String, padding: UInt) { - assert(padding < Int.MAX_VALUE.toUInt()) - writePaddedFile(inBF, srcFile, padding.toInt()) - } - - private fun writePaddedFile(inBF: ByteBuffer, srcFile: String, padding: Int) { - FileInputStream(srcFile).use { iS -> - var byteRead: Int - val dataRead = ByteArray(128) - while (true) { - byteRead = iS.read(dataRead) - if (-1 == byteRead) { - break - } - inBF.put(dataRead, 0, byteRead) - } - padFile(inBF, padding) - } - } - - private fun padFile(inBF: ByteBuffer, padding: Int) { - val pad = padding - (inBF.position() and padding - 1) and padding - 1 - inBF.put(ByteArray(pad)) - } - - private fun writeData(info2: BootImgInfo, outputFile: String) { - log.info("Writing data ...") - val param = ParamConfig() - - val bf = ByteBuffer.allocate(1024 * 1024 * 64)//assume total SIZE small than 64MB - bf.order(ByteOrder.LITTLE_ENDIAN) - - writePaddedFile(bf, param.kernel, info2.pageSize) - if (info2.ramdiskLength > 0U) { - writePaddedFile(bf, param.ramdisk!!, info2.pageSize) - } - if (info2.secondBootloaderLength > 0U) { - writePaddedFile(bf, param.second!!, info2.pageSize) - } - if (info2.recoveryDtboLength > 0U) { - writePaddedFile(bf, param.dtbo!!, info2.pageSize) - } - if (info2.dtbLength > 0U) { - writePaddedFile(bf, param.dtb!!, info2.pageSize) - } - //write - FileOutputStream("$outputFile.clear", true).use { fos -> - fos.write(bf.array(), 0, bf.position()) - } - } - - private fun packRootfs(mkbootfs: String) { - val param = ParamConfig() - log.info("Packing rootfs ${UnifiedConfig.workDir}root ...") - val outputStream = ByteArrayOutputStream() - val exec = DefaultExecutor() - exec.streamHandler = PumpStreamHandler(outputStream) - val cmdline = "$mkbootfs ${UnifiedConfig.workDir}root" - log.info(cmdline) - exec.execute(CommandLine.parse(cmdline)) - Helper.gnuZipFile2(param.ramdisk!!, ByteArrayInputStream(outputStream.toByteArray())) - log.info("${param.ramdisk} is ready") - } - - private fun File.deleleIfExists() { - if (this.exists()) { - if (!this.isFile) { - throw IllegalStateException("${this.canonicalPath} should be regular file") - } - log.info("Deleting ${this.path} ...") - this.delete() - } - } - - fun pack(mkbootfsBin: String) { - val param = ParamConfig() - log.info("Loading config from ${param.cfg}") - val cfg = ObjectMapper().readValue(File(param.cfg), UnifiedConfig::class.java) - val info2 = cfg.toBootImgInfo() - - //clean - File(cfg.info.output + ".google").deleleIfExists() - File(cfg.info.output + ".clear").deleleIfExists() - File(cfg.info.output + ".signed").deleleIfExists() - File(cfg.info.output + ".signed2").deleleIfExists() - File("${UnifiedConfig.workDir}ramdisk.img").deleleIfExists() - - if (info2.ramdiskLength > 0U) { - if (File(param.ramdisk!!).exists() && !File(UnifiedConfig.workDir + "root").exists()) { - //do nothing if we have ramdisk.img.gz but no /root - log.warn("Use prebuilt ramdisk file: ${param.ramdisk}") - } else { - File(param.ramdisk!!).deleleIfExists() - packRootfs(mkbootfsBin) - } - } - - val encodedHeader = info2.encode() - //write - FileOutputStream(cfg.info.output + ".clear", false).use { fos -> - fos.write(encodedHeader) - fos.write(ByteArray((Helper.round_to_multiple(encodedHeader.size.toUInt(), info2.pageSize) - encodedHeader.size.toUInt()).toInt())) - } - writeData(info2, cfg.info.output) - - val googleCmd = info2.toCommandLine().apply { - addArgument(cfg.info.output + ".google") - } - log.info(googleCmd.toString()) - DefaultExecutor().execute(googleCmd) - - val ourHash = hashFileAndSize(cfg.info.output + ".clear") - val googleHash = hashFileAndSize(cfg.info.output + ".google") - log.info("ours hash ${Helper.toHexString(ourHash)}, google's hash ${Helper.toHexString(googleHash)}") - if (ourHash.contentEquals(googleHash)) { - log.info("Hash verification passed: ${Helper.toHexString(ourHash)}") - } else { - log.error("Hash verification failed") - throw UnknownError("Do not know why hash verification fails, maybe a bug") - } - } -} diff --git a/bbootimg/src/main/kotlin/bootimg/Parser.kt b/bbootimg/src/main/kotlin/bootimg/Parser.kt deleted file mode 100644 index 6c78d27..0000000 --- a/bbootimg/src/main/kotlin/bootimg/Parser.kt +++ /dev/null @@ -1,163 +0,0 @@ -package cfig - -import cfig.bootimg.BootImgInfo -import cfig.dtb_util.DTC -import cfig.kernel_util.KernelExtractor -import com.fasterxml.jackson.databind.ObjectMapper -import org.apache.commons.exec.CommandLine -import org.apache.commons.exec.DefaultExecutor -import org.slf4j.LoggerFactory -import java.io.File -import java.io.FileInputStream - -@OptIn(ExperimentalUnsignedTypes::class) -class Parser { - private fun verifiedWithAVB(fileName: String): Boolean { - val expectedBf = "AVBf".toByteArray() - FileInputStream(fileName).use { fis -> - fis.skip(File(fileName).length() - 64) - val bf = ByteArray(4) - fis.read(bf) - return bf.contentEquals(expectedBf) - } - } - - private fun unpackRamdisk(workDir: String, ramdiskGz: String) { - val exe = DefaultExecutor() - exe.workingDirectory = File(workDir + "root") - if (exe.workingDirectory.exists()) exe.workingDirectory.deleteRecursively() - exe.workingDirectory.mkdirs() - val ramdiskFile = File(ramdiskGz.removeSuffix(".gz")) - exe.execute(CommandLine.parse("cpio -i -m -F " + ramdiskFile.canonicalPath)) - log.info(" ramdisk extracted : $ramdiskFile -> ${exe.workingDirectory.path}") - } - - fun parseBootImgHeader(fileName: String, avbtool: String): BootImgInfo { - val info2 = BootImgInfo(FileInputStream(fileName)) - val param = ParamConfig() - if (verifiedWithAVB(fileName)) { - info2.signatureType = BootImgInfo.VerifyType.AVB - verifyAVBIntegrity(fileName, avbtool) - } else { - info2.signatureType = BootImgInfo.VerifyType.VERIFY - } - info2.imageSize = File(fileName).length() - - val cfg = UnifiedConfig.fromBootImgInfo(info2).apply { - info.output = File(fileName).name - } - - ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(File(param.cfg), cfg) - log.info("image info written to ${param.cfg}") - - return info2 - } - - private fun parseKernelInfo(kernelFile: String) { - val ke = KernelExtractor() - if (ke.envCheck()) { - ke.run(kernelFile, File(".")) - } - } - - fun extractBootImg(fileName: String, info2: BootImgInfo) { - val param = ParamConfig() - - InfoTable.instance.addRule() - if (info2.kernelLength > 0U) { - Helper.extractFile(fileName, - param.kernel, - info2.kernelPosition.toLong(), - info2.kernelLength.toInt()) - log.info(" kernel dumped to: ${param.kernel}, size=${info2.kernelLength.toInt() / 1024.0 / 1024.0}MB") - InfoTable.instance.addRow("kernel", param.kernel) - parseKernelInfo(param.kernel) - } else { - throw RuntimeException("bad boot image: no kernel found") - } - - if (info2.ramdiskLength > 0U) { - Helper.extractFile(fileName, - param.ramdisk!!, - info2.ramdiskPosition.toLong(), - info2.ramdiskLength.toInt()) - log.info("ramdisk dumped to: ${param.ramdisk}") - Helper.unGnuzipFile(param.ramdisk!!, param.ramdisk!!.removeSuffix(".gz")) - unpackRamdisk(UnifiedConfig.workDir, param.ramdisk!!.removeSuffix(".gz")) - InfoTable.instance.addRule() - InfoTable.instance.addRow("ramdisk", param.ramdisk!!.removeSuffix(".gz")) - InfoTable.instance.addRow("\\-- extracted ramdisk rootfs", "${UnifiedConfig.workDir}root") - } else { - InfoTable.missingParts.add("ramdisk") - log.info("no ramdisk found") - } - - if (info2.secondBootloaderLength > 0U) { - Helper.extractFile(fileName, - param.second!!, - info2.secondBootloaderPosition.toLong(), - info2.secondBootloaderLength.toInt()) - log.info("second bootloader dumped to ${param.second}") - InfoTable.instance.addRule() - InfoTable.instance.addRow("second bootloader", param.second) - } else { - InfoTable.missingParts.add("second bootloader") - log.info("no second bootloader found") - } - - if (info2.recoveryDtboLength > 0U) { - Helper.extractFile(fileName, - param.dtbo!!, - info2.recoveryDtboPosition.toLong(), - info2.recoveryDtboLength.toInt()) - log.info("recovery dtbo dumped to ${param.dtbo}") - InfoTable.instance.addRule() - InfoTable.instance.addRow("recovery dtbo", param.dtbo) - } else { - InfoTable.missingParts.add("recovery dtbo") - if (info2.headerVersion > 0U) { - log.info("no recovery dtbo found") - } else { - log.debug("no recovery dtbo for header v0") - } - } - - if (info2.dtbLength > 0U) { - Helper.extractFile(fileName, - param.dtb!!, - info2.dtbPosition.toLong(), - info2.dtbLength.toInt()) - log.info("dtb dumped to ${param.dtb}") - InfoTable.instance.addRule() - InfoTable.instance.addRow("dtb", param.dtb) - //extract DTB - if (EnvironmentVerifier().hasDtc) { - if (DTC().decompile(param.dtb!!, param.dtb + ".src")) { - InfoTable.instance.addRow("\\-- decompiled dts", param.dtb + ".src") - } - } - //extract DTB - } else { - InfoTable.missingParts.add("dtb") - if (info2.headerVersion > 1U) { - log.info("no dtb found") - } else { - log.debug("no dtb for header v0") - } - } - } - - companion object { - private val log = LoggerFactory.getLogger("Parser")!! - - fun verifyAVBIntegrity(fileName: String, avbtool: String) { - val cmdline = "$avbtool verify_image --image $fileName" - log.info(cmdline) - try { - DefaultExecutor().execute(CommandLine.parse(cmdline)) - } catch (e: Exception) { - throw IllegalArgumentException("$fileName failed integrity check by \"$cmdline\"") - } - } - } -} diff --git a/bbootimg/src/main/kotlin/bootimg/Signer.kt b/bbootimg/src/main/kotlin/bootimg/Signer.kt new file mode 100644 index 0000000..c3858b0 --- /dev/null +++ b/bbootimg/src/main/kotlin/bootimg/Signer.kt @@ -0,0 +1,87 @@ +package cfig.bootimg + +import avb.AVBInfo +import avb.alg.Algorithms +import cfig.Avb +import cfig.Avb.Companion.getJsonFileName +import cfig.Helper +import com.fasterxml.jackson.databind.ObjectMapper +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.DefaultExecutor +import org.slf4j.LoggerFactory +import java.io.File + +class Signer { + @OptIn(ExperimentalUnsignedTypes::class) + companion object { + private val log = LoggerFactory.getLogger(Signer::class.java) + + fun signAVB(output: String, imageSize: Long) { + val avbtool = Helper.prop("avbtool") + log.info("Adding hash_footer with verified-boot 2.0 style") + val ai = ObjectMapper().readValue(File(getJsonFileName(output)), AVBInfo::class.java) + val alg = Algorithms.get(ai.header!!.algorithm_type.toInt()) + val bootDesc = ai.auxBlob!!.hashDescriptors[0] + val newAvbInfo = ObjectMapper().readValue(File(getJsonFileName(output)), AVBInfo::class.java) + + //our signer + File("$output.clear").copyTo(File("$output.signed"), overwrite = true) + Avb().addHashFooter("$output.signed", + imageSize, + partition_name = bootDesc.partition_name, + newAvbInfo = newAvbInfo) + //original signer + CommandLine.parse("$avbtool add_hash_footer").apply { + addArguments("--image ${output}.signed2") + addArguments("--partition_size ${imageSize}") + addArguments("--salt ${Helper.toHexString(bootDesc.salt)}") + addArguments("--partition_name ${bootDesc.partition_name}") + addArguments("--hash_algorithm ${bootDesc.hash_algorithm}") + addArguments("--algorithm ${alg!!.name}") + if (alg.defaultKey.isNotBlank()) { + addArguments("--key ${alg.defaultKey}") + } + newAvbInfo.auxBlob?.let { newAuxblob -> + newAuxblob.propertyDescriptor.forEach { newProp -> + addArguments(arrayOf("--prop", "${newProp.key}:${newProp.value}")) + } + } + addArgument("--internal_release_string") + addArgument(ai.header!!.release_string, false) + log.info(this.toString()) + + File("$output.clear").copyTo(File("$output.signed2"), overwrite = true) + DefaultExecutor().execute(this) + } + Common.assertFileEquals("$output.signed", "$output.signed2") + //TODO: decide what to verify + //Parser.verifyAVBIntegrity(cfg.info.output + ".signed", avbtool) + //Parser.verifyAVBIntegrity(cfg.info.output + ".signed2", avbtool) + } + + fun signVB1(src: String, tgt: String) { + val bootSigner = Helper.prop("bootSigner") + log.info("Signing with verified-boot 1.0 style") + val sig = Common.VeritySignature( + verity_pk8 = Helper.prop("verity_pk8"), + verity_pem = Helper.prop("verity_pem"), + jarPath = Helper.prop("bootSigner") + ) + val bootSignCmd = "java -jar $bootSigner " + + "${sig.path} $src " + + "${sig.verity_pk8} ${sig.verity_pem} " + + "$tgt" + log.info(bootSignCmd) + DefaultExecutor().execute(CommandLine.parse(bootSignCmd)) + } + + fun mapToJson(m: LinkedHashMap<*, *>): String { + val sb = StringBuilder() + m.forEach { k, v -> + if (sb.isNotEmpty()) sb.append(", ") + sb.append("\"$k\": \"$v\"") + } + return "{ $sb }" + } + } +} diff --git a/bbootimg/src/main/kotlin/bootimg/v2/BootHeaderV2.kt b/bbootimg/src/main/kotlin/bootimg/v2/BootHeaderV2.kt new file mode 100644 index 0000000..e4b565e --- /dev/null +++ b/bbootimg/src/main/kotlin/bootimg/v2/BootHeaderV2.kt @@ -0,0 +1,162 @@ +package cfig.bootimg.v2 + +import cfig.Helper +import cfig.bootimg.Common +import cfig.io.Struct3 +import org.slf4j.LoggerFactory +import java.io.InputStream +import kotlin.math.pow + +@OptIn(ExperimentalUnsignedTypes::class) +open class BootHeaderV2( + var kernelLength: UInt = 0U, + var kernelOffset: UInt = 0U, + + var ramdiskLength: UInt = 0U, + var ramdiskOffset: UInt = 0U, + + var secondBootloaderLength: UInt = 0U, + var secondBootloaderOffset: UInt = 0U, + + var recoveryDtboLength: UInt = 0U, + var recoveryDtboOffset: ULong = 0UL,//Q + + var dtbLength: UInt = 0U, + var dtbOffset: ULong = 0UL,//Q + + var tagsOffset: UInt = 0U, + + var pageSize: UInt = 0U, + + var headerSize: UInt = 0U, + var headerVersion: UInt = 0U, + + var board: String = "", + + var cmdline: String = "", + + var hash: ByteArray? = null, + + var osVersion: String? = null, + var osPatchLevel: String? = null) { + @Throws(IllegalArgumentException::class) + constructor(iS: InputStream?) : this() { + if (iS == null) { + return + } + log.warn("BootImgHeader constructor") + val info = Struct3(FORMAT_STRING).unpack(iS) + assert(20 == info.size) + if (info[0] != magic) { + throw IllegalArgumentException("stream doesn't look like Android Boot Image Header") + } + this.kernelLength = info[1] as UInt + this.kernelOffset = info[2] as UInt + this.ramdiskLength = info[3] as UInt + this.ramdiskOffset = info[4] as UInt + this.secondBootloaderLength = info[5] as UInt + this.secondBootloaderOffset = info[6] as UInt + this.tagsOffset = info[7] as UInt + this.pageSize = info[8] as UInt + this.headerVersion = info[9] as UInt + val osNPatch = info[10] as UInt + if (0U != osNPatch) { //treated as 'reserved' in this boot image + this.osVersion = Common.parseOsVersion(osNPatch.toInt() shr 11) + this.osPatchLevel = Common.parseOsPatchLevel((osNPatch and 0x7ff.toUInt()).toInt()) + } + this.board = info[11] as String + this.cmdline = (info[12] as String) + (info[14] as String) + this.hash = info[13] as ByteArray + + if (this.headerVersion > 0U) { + this.recoveryDtboLength = info[15] as UInt + this.recoveryDtboOffset = info[16] as ULong + } + + this.headerSize = info[17] as UInt + assert(this.headerSize.toInt() in intArrayOf(BOOT_IMAGE_HEADER_V2_SIZE, + BOOT_IMAGE_HEADER_V1_SIZE, BOOT_IMAGE_HEADER_V0_SIZE)) { + "header size ${this.headerSize} illegal" + } + + if (this.headerVersion > 1U) { + this.dtbLength = info[18] as UInt + this.dtbOffset = info[19] as ULong + } + } + + private fun get_recovery_dtbo_offset(): UInt { + return Helper.round_to_multiple(this.headerSize, pageSize) + + Helper.round_to_multiple(this.kernelLength, pageSize) + + Helper.round_to_multiple(this.ramdiskLength, pageSize) + + Helper.round_to_multiple(this.secondBootloaderLength, pageSize) + } + + fun encode(): ByteArray { + val pageSizeChoices: MutableSet = mutableSetOf().apply { + (11..14).forEach { add(2.0.pow(it).toLong()) } + } + assert(pageSizeChoices.contains(pageSize.toLong())) { "invalid parameter [pageSize=$pageSize], (choose from $pageSizeChoices)" } + return Struct3(FORMAT_STRING).pack( + magic, + //10I + kernelLength, + kernelOffset, + ramdiskLength, + ramdiskOffset, + secondBootloaderLength, + secondBootloaderOffset, + tagsOffset, + pageSize, + headerVersion, + (Common.packOsVersion(osVersion) shl 11) or Common.packOsPatchLevel(osPatchLevel), + //16s + board, + //512s + cmdline.substring(0, minOf(512, cmdline.length)), + //32b + hash!!, + //1024s + if (cmdline.length > 512) cmdline.substring(512) else "", + //I + recoveryDtboLength, + //Q + if (headerVersion > 0U) recoveryDtboOffset else 0, + //I + when (headerVersion) { + 0U -> BOOT_IMAGE_HEADER_V0_SIZE + 1U -> BOOT_IMAGE_HEADER_V1_SIZE + 2U -> BOOT_IMAGE_HEADER_V2_SIZE + else -> java.lang.IllegalArgumentException("headerVersion $headerVersion illegal") + }, + //I + dtbLength, + //Q + if (headerVersion > 1U) dtbOffset else 0 + ) + } + + companion object { + internal val log = LoggerFactory.getLogger(BootHeaderV2::class.java) + const val magic = "ANDROID!" + const val FORMAT_STRING = "8s" + //"ANDROID!" + "10I" + + "16s" + //board name + "512s" + //cmdline part 1 + "32b" + //hash digest + "1024s" + //cmdline part 2 + "I" + //dtbo length [v1] + "Q" + //dtbo offset [v1] + "I" + //header size [v1] + "I" + //dtb length [v2] + "Q" //dtb offset [v2] + const val BOOT_IMAGE_HEADER_V2_SIZE = 1660 + const val BOOT_IMAGE_HEADER_V1_SIZE = 1648 + const val BOOT_IMAGE_HEADER_V0_SIZE = 0 + + init { + assert(BOOT_IMAGE_HEADER_V2_SIZE == Struct3(FORMAT_STRING).calcSize()) + } + + } +} diff --git a/bbootimg/src/main/kotlin/bootimg/v2/BootV2.kt b/bbootimg/src/main/kotlin/bootimg/v2/BootV2.kt new file mode 100644 index 0000000..9ec43b9 --- /dev/null +++ b/bbootimg/src/main/kotlin/bootimg/v2/BootV2.kt @@ -0,0 +1,470 @@ +package cfig.bootimg.v2 + +import cfig.Avb +import cfig.Helper +import cfig.bootimg.Common +import cfig.bootimg.Common.Companion.deleleIfExists +import cfig.bootimg.Common.Slice +import cfig.bootimg.Signer +import cfig.packable.VBMetaParser +import com.fasterxml.jackson.databind.ObjectMapper +import de.vandermeer.asciitable.AsciiTable +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.DefaultExecutor +import org.slf4j.LoggerFactory +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder + +@OptIn(ExperimentalUnsignedTypes::class) +data class BootV2( + var info: MiscInfo = MiscInfo(), + var kernel: CommArgs = CommArgs(), + var ramdisk: CommArgs = CommArgs(), + var secondBootloader: CommArgs? = null, + var recoveryDtbo: CommArgsLong? = null, + var dtb: CommArgsLong? = null +) { + data class MiscInfo( + var output: String = "", + var json: String = "", + var headerVersion: UInt = 0U, + var headerSize: UInt = 0U, + var loadBase: UInt = 0U, + var tagsOffset: UInt = 0U, + var board: String? = null, + var pageSize: UInt = 0U, + var cmdline: String = "", + var osVersion: String? = null, + var osPatchLevel: String? = null, + var hash: ByteArray? = byteArrayOf(), + var verify: String = "", + var imageSize: Long = 0) + + data class CommArgs( + var file: String? = null, + var position: UInt = 0U, + var size: Int = 0, + var loadOffset: UInt = 0U) + + data class CommArgsLong( + var file: String? = null, + var position: UInt = 0U, + var size: UInt = 0U, + var loadOffset: ULong = 0U) + + companion object { + private val log = LoggerFactory.getLogger(BootV2::class.java) + private val workDir = Helper.prop("workDir") + + fun parse(fileName: String): BootV2 { + val ret = BootV2() + FileInputStream(fileName).use { fis -> + val bh2 = BootHeaderV2(fis) + ret.info.let { theInfo -> + theInfo.output = File(fileName).name + theInfo.json = File(fileName).name.removeSuffix(".img") + ".json" + theInfo.pageSize = bh2.pageSize + theInfo.headerSize = bh2.headerSize + theInfo.headerVersion = bh2.headerVersion + theInfo.board = bh2.board + theInfo.cmdline = bh2.cmdline + theInfo.imageSize = File(fileName).length() + theInfo.tagsOffset = bh2.tagsOffset + theInfo.hash = bh2.hash + theInfo.osVersion = bh2.osVersion + theInfo.osPatchLevel = bh2.osPatchLevel + if (Avb.hasAvbFooter(fileName)) { + theInfo.verify = "VB2.0" + Avb.verifyAVBIntegrity(fileName, Helper.prop("avbtool")) + } else { + theInfo.verify = "VB1.0" + } + } + ret.kernel.let { theKernel -> + theKernel.file = "${workDir}kernel" + theKernel.size = bh2.kernelLength.toInt() + theKernel.loadOffset = bh2.kernelOffset + theKernel.position = ret.getKernelPosition() + } + ret.ramdisk.let { theRamdisk -> + theRamdisk.size = bh2.ramdiskLength.toInt() + theRamdisk.loadOffset = bh2.ramdiskOffset + theRamdisk.position = ret.getRamdiskPosition() + if (bh2.ramdiskLength > 0U) { + theRamdisk.file = "${workDir}ramdisk.img.gz" + } + } + if (bh2.secondBootloaderLength > 0U) { + ret.secondBootloader = CommArgs() + ret.secondBootloader!!.size = bh2.secondBootloaderLength.toInt() + ret.secondBootloader!!.loadOffset = bh2.secondBootloaderOffset + ret.secondBootloader!!.file = "${workDir}second" + ret.secondBootloader!!.position = ret.getSecondBootloaderPosition() + } + if (bh2.recoveryDtboLength > 0U) { + ret.recoveryDtbo = CommArgsLong() + ret.recoveryDtbo!!.size = bh2.recoveryDtboLength + ret.recoveryDtbo!!.loadOffset = bh2.recoveryDtboOffset //Q + ret.recoveryDtbo!!.file = "${workDir}recoveryDtbo" + ret.recoveryDtbo!!.position = ret.getRecoveryDtboPosition().toUInt() + } + if (bh2.dtbLength > 0U) { + ret.dtb = CommArgsLong() + ret.dtb!!.size = bh2.dtbLength + ret.dtb!!.loadOffset = bh2.dtbOffset //Q + ret.dtb!!.file = "${workDir}dtb" + ret.dtb!!.position = ret.getDtbPosition() + } + } + return ret + } + } + + private fun getHeaderSize(pageSize: UInt): UInt { + val pad = (pageSize - (1648U and (pageSize - 1U))) and (pageSize - 1U) + return pad + 1648U + } + + private fun getKernelPosition(): UInt { + return getHeaderSize(info.pageSize) + } + + private fun getRamdiskPosition(): UInt { + return (getKernelPosition() + kernel.size.toUInt() + + Common.getPaddingSize(kernel.size.toUInt(), info.pageSize)) + } + + private fun getSecondBootloaderPosition(): UInt { + return getRamdiskPosition() + ramdisk.size.toUInt() + + Common.getPaddingSize(ramdisk.size.toUInt(), info.pageSize) + } + + private fun getRecoveryDtboPosition(): UInt { + return if (this.secondBootloader == null) { + getSecondBootloaderPosition() + } else { + getSecondBootloaderPosition() + secondBootloader!!.size.toUInt() + + Common.getPaddingSize(secondBootloader!!.size.toUInt(), info.pageSize) + } + } + + private fun getDtbPosition(): UInt { + return if (this.recoveryDtbo == null) { + getRecoveryDtboPosition() + } else { + getRecoveryDtboPosition() + recoveryDtbo!!.size + + Common.getPaddingSize(recoveryDtbo!!.size, info.pageSize) + } + } + + fun extractImages(): BootV2 { + val workDir = Helper.prop("workDir") + //info + ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(File(workDir + info.json), this) + //kernel + Common.dumpKernel(Slice(info.output, kernel.position.toInt(), kernel.size, kernel.file!!)) + //ramdisk + if (this.ramdisk.size > 0) { + Common.dumpRamdisk(Slice(info.output, ramdisk.position.toInt(), ramdisk.size, ramdisk.file!!), + "${workDir}root") + } + //second bootloader + secondBootloader?.let { + Helper.extractFile(info.output, + secondBootloader!!.file!!, + secondBootloader!!.position.toLong(), + secondBootloader!!.size) + } + //recovery dtbo + recoveryDtbo?.let { + Helper.extractFile(info.output, + recoveryDtbo!!.file!!, + recoveryDtbo!!.position.toLong(), + recoveryDtbo!!.size.toInt()) + } + //dtb + this.dtb?.let { _ -> + Common.dumpDtb(Slice(info.output, dtb!!.position.toInt(), dtb!!.size.toInt(), dtb!!.file!!)) + } + + return this + } + + fun extractVBMeta(): BootV2 { + Avb().parseVbMeta(info.output) + if (File("vbmeta.img").exists()) { + log.warn("Found vbmeta.img, parsing ...") + VBMetaParser().unpack("vbmeta.img") + } + return this + } + + fun printSummary(): BootV2 { + val workDir = Helper.prop("workDir") + val tableHeader = AsciiTable().apply { + addRule() + addRow("What", "Where") + addRule() + } + val tab = AsciiTable().let { + it.addRule() + it.addRow("image info", workDir + info.output.removeSuffix(".img") + ".json") + if (this.info.verify == "VB2.0") { + it.addRule() + it.addRow("AVB info", Avb.getJsonFileName(info.output)) + } + //kernel + it.addRule() + it.addRow("kernel", this.kernel.file) + File(Helper.prop("kernelVersionFile")).let { kernelVersionFile -> + if (kernelVersionFile.exists()) { + it.addRow("\\-- version " + kernelVersionFile.readLines().toString(), kernelVersionFile.path) + } + } + File(Helper.prop("kernelConfigFile")).let { kernelConfigFile -> + if (kernelConfigFile.exists()) { + it.addRow("\\-- config", kernelConfigFile.path) + } + } + //ramdisk + if (this.ramdisk.size > 0) { + it.addRule() + it.addRow("ramdisk", this.ramdisk.file) + it.addRow("\\-- extracted ramdisk rootfs", "${workDir}root") + } + //second + this.secondBootloader?.let { theSecondBootloader -> + if (theSecondBootloader.size > 0) { + it.addRule() + it.addRow("second bootloader", theSecondBootloader.file) + } + } + //dtbo + this.recoveryDtbo?.let { theDtbo -> + if (theDtbo.size > 0U) { + it.addRule() + it.addRow("recovery dtbo", theDtbo.file) + } + } + //dtb + this.dtb?.let { theDtb -> + if (theDtb.size > 0u) { + it.addRule() + it.addRow("dtb", theDtb.file) + if (File(theDtb.file + ".src").exists()) { + it.addRow("\\-- decompiled dts", theDtb.file + ".src") + } + } + } + //END + it.addRule() + it + } + val tabVBMeta = AsciiTable().let { + if (File("vbmeta.img").exists()) { + it.addRule() + it.addRow("vbmeta.img", Avb.getJsonFileName("vbmeta.img")) + it.addRule() + "\n" + it.render() + } else { + "" + } + } + log.info("\n\t\t\tUnpack Summary of ${info.output}\n{}\n{}{}", + tableHeader.render(), tab.render(), tabVBMeta) + return this + } + + private fun toHeader(): BootHeaderV2 { + return BootHeaderV2( + kernelLength = kernel.size.toUInt(), + kernelOffset = kernel.loadOffset, + ramdiskLength = ramdisk.size.toUInt(), + ramdiskOffset = ramdisk.loadOffset, + secondBootloaderLength = if (secondBootloader != null) secondBootloader!!.size.toUInt() else 0U, + secondBootloaderOffset = if (secondBootloader != null) secondBootloader!!.loadOffset else 0U, + recoveryDtboLength = if (recoveryDtbo != null) recoveryDtbo!!.size.toUInt() else 0U, + recoveryDtboOffset = if (recoveryDtbo != null) recoveryDtbo!!.loadOffset else 0U, + dtbLength = if (dtb != null) dtb!!.size else 0U, + dtbOffset = if (dtb != null) dtb!!.loadOffset else 0U, + tagsOffset = info.tagsOffset, + pageSize = info.pageSize, + headerSize = info.headerSize, + headerVersion = info.headerVersion, + board = info.board.toString(), + cmdline = info.cmdline, + hash = info.hash, + osVersion = info.osVersion, + osPatchLevel = info.osPatchLevel + ) + } + + fun pack(): BootV2 { + //refresh kernel size + this.kernel.size = File(this.kernel.file!!).length().toInt() + //refresh ramdisk size + if (this.ramdisk.file.isNullOrBlank()) { + ramdisk.file = null + ramdisk.loadOffset = 0U + } else { + if (File(this.ramdisk.file!!).exists() && !File(workDir + "root").exists()) { + //do nothing if we have ramdisk.img.gz but no /root + log.warn("Use prebuilt ramdisk file: ${this.ramdisk.file}") + } else { + File(this.ramdisk.file!!).deleleIfExists() + File(this.ramdisk.file!!.removeSuffix(".gz")).deleleIfExists() + Common.packRootfs("${workDir}/root", this.ramdisk.file!!) + } + this.ramdisk.size = File(this.ramdisk.file!!).length().toInt() + } + //refresh second bootloader size + secondBootloader?.let { theSecond -> + theSecond.size = File(theSecond.file!!).length().toInt() + } + //refresh recovery dtbo size + recoveryDtbo?.let { theDtbo -> + theDtbo.size = File(theDtbo.file!!).length().toUInt() + theDtbo.loadOffset = getRecoveryDtboPosition().toULong() + log.warn("using fake recoveryDtboOffset ${theDtbo.loadOffset} (as is in AOSP avbtool)") + } + //refresh dtb size + dtb?.let { theDtb -> + theDtb.size = File(theDtb.file!!).length().toUInt() + } + //refresh image hash + info.hash = when (info.headerVersion) { + 0U -> { + Common.hashFileAndSize(kernel.file, ramdisk.file, secondBootloader?.file) + } + 1U -> { + Common.hashFileAndSize(kernel.file, ramdisk.file, + secondBootloader?.file, recoveryDtbo?.file) + } + 2U -> { + Common.hashFileAndSize(kernel.file, ramdisk.file, + secondBootloader?.file, recoveryDtbo?.file, dtb?.file) + } + else -> { + throw IllegalArgumentException("headerVersion ${info.headerVersion} illegal") + } + } + + val encodedHeader = this.toHeader().encode() + + //write + FileOutputStream("${info.output}.clear", false).use { fos -> + fos.write(encodedHeader) + fos.write(ByteArray((Helper.round_to_multiple(encodedHeader.size.toUInt(), info.pageSize) - encodedHeader.size.toUInt()).toInt())) + } + + log.info("Writing data ...") + val bytesV2 = ByteBuffer.allocate(1024 * 1024 * 64)//assume total SIZE small than 64MB + .let { bf -> + bf.order(ByteOrder.LITTLE_ENDIAN) + Common.writePaddedFile(bf, kernel.file!!, info.pageSize) + if (ramdisk.size > 0) { + Common.writePaddedFile(bf, ramdisk.file!!, info.pageSize) + } + secondBootloader?.let { + Common.writePaddedFile(bf, secondBootloader!!.file!!, info.pageSize) + } + recoveryDtbo?.let { + Common.writePaddedFile(bf, recoveryDtbo!!.file!!, info.pageSize) + } + dtb?.let { + Common.writePaddedFile(bf, dtb!!.file!!, info.pageSize) + } + bf + } + //write + FileOutputStream("${info.output}.clear", true).use { fos -> + fos.write(bytesV2.array(), 0, bytesV2.position()) + } + + this.toCommandLine().apply { + addArgument("${info.output}.google") + log.info(this.toString()) + DefaultExecutor().execute(this) + } + + Common.assertFileEquals("${info.output}.clear", "${info.output}.google") + + return this + } + + private fun toCommandLine(): CommandLine { + val ret = CommandLine(Helper.prop("mkbootimg")) + ret.addArgument(" --header_version ") + ret.addArgument(info.headerVersion.toString()) + ret.addArgument(" --base ") + ret.addArgument("0x" + java.lang.Long.toHexString(0)) + ret.addArgument(" --kernel ") + ret.addArgument(kernel.file!!) + ret.addArgument(" --kernel_offset ") + ret.addArgument("0x" + Integer.toHexString(kernel.loadOffset.toInt())) + if (this.ramdisk.size > 0) { + ret.addArgument(" --ramdisk ") + ret.addArgument(ramdisk.file) + } + ret.addArgument(" --ramdisk_offset ") + ret.addArgument("0x" + Integer.toHexString(ramdisk.loadOffset.toInt())) + if (secondBootloader != null) { + ret.addArgument(" --second ") + ret.addArgument(secondBootloader!!.file!!) + ret.addArgument(" --second_offset ") + ret.addArgument("0x" + Integer.toHexString(secondBootloader!!.loadOffset.toInt())) + } + if (!info.board.isNullOrBlank()) { + ret.addArgument(" --board ") + ret.addArgument(info.board) + } + if (info.headerVersion > 0U) { + if (recoveryDtbo != null) { + ret.addArgument(" --recovery_dtbo ") + ret.addArgument(recoveryDtbo!!.file!!) + } + } + if (info.headerVersion > 1U) { + if (dtb != null) { + ret.addArgument("--dtb ") + ret.addArgument(dtb!!.file!!) + ret.addArgument("--dtb_offset ") + ret.addArgument("0x" + java.lang.Long.toHexString(dtb!!.loadOffset.toLong())) + } + } + ret.addArgument(" --pagesize ") + ret.addArgument(Integer.toString(info.pageSize.toInt())) + ret.addArgument(" --cmdline ") + ret.addArgument(info.cmdline, false) + if (!info.osVersion.isNullOrBlank()) { + ret.addArgument(" --os_version ") + ret.addArgument(info.osVersion) + } + if (!info.osPatchLevel.isNullOrBlank()) { + ret.addArgument(" --os_patch_level ") + ret.addArgument(info.osPatchLevel) + } + ret.addArgument(" --tags_offset ") + ret.addArgument("0x" + Integer.toHexString(info.tagsOffset.toInt())) + ret.addArgument(" --id ") + ret.addArgument(" --output ") + //ret.addArgument("boot.img" + ".google") + + log.debug("To Commandline: $ret") + + return ret + } + + fun sign(): BootV2 { + if (info.verify == "VB2.0") { + Signer.signAVB(info.output, this.info.imageSize) + log.info("Adding hash_footer with verified-boot 2.0 style") + } else { + Signer.signVB1(info.output + ".clear", info.output + ".signed") + } + return this + } +} diff --git a/bbootimg/src/main/kotlin/bootimg/v3/BootHeaderV3.kt b/bbootimg/src/main/kotlin/bootimg/v3/BootHeaderV3.kt new file mode 100644 index 0000000..a337f7a --- /dev/null +++ b/bbootimg/src/main/kotlin/bootimg/v3/BootHeaderV3.kt @@ -0,0 +1,81 @@ +package cfig.bootimg.v3 + +import cfig.bootimg.Common +import cfig.io.Struct3 +import org.slf4j.LoggerFactory +import java.io.InputStream + +@OptIn(ExperimentalUnsignedTypes::class) +class BootHeaderV3( + var kernelSize: UInt = 0U, + var ramdiskSize: UInt = 0U, + var osVersion: String = "", + var osPatchLevel: String = "", + var headerSize: UInt = 0U, + var headerVersion: UInt = 0U, + var cmdline: String = "" +) { + @Throws(IllegalArgumentException::class) + constructor(iS: InputStream?) : this() { + if (iS == null) { + return + } + log.warn("BootImgHeaderV3 constructor") + val info = Struct3(FORMAT_STRING).unpack(iS) + assert(11 == info.size) + if (info[0] != magic) { + throw IllegalArgumentException("stream doesn't look like Android Boot Image V3 Header") + } + this.kernelSize = info[1] as UInt + this.ramdiskSize = info[2] as UInt + val osNPatch = info[3] as UInt + if (0U != osNPatch) { //treated as 'reserved' in this boot image + this.osVersion = Common.parseOsVersion(osNPatch.toInt() shr 11) + this.osPatchLevel = Common.parseOsPatchLevel((osNPatch and 0x7ff.toUInt()).toInt()) + } + this.headerSize = info[4] as UInt + //5,6,7,8 reserved + this.headerVersion = info[9] as UInt + + this.cmdline = info[10] as String + + assert(this.headerSize.toInt() in intArrayOf(BOOT_IMAGE_HEADER_V3_SIZE)) + } + + fun encode(): ByteArray { + return Struct3(FORMAT_STRING).pack( + magic, + kernelSize, + ramdiskSize, + (Common.packOsVersion(osVersion) shl 11) or Common.packOsPatchLevel(osPatchLevel), + headerSize, + 0, + 0, + 0, + 0, + headerVersion, + cmdline) + } + + override fun toString(): String { + return "BootImgHeaderV3(kernelSize=$kernelSize, ramdiskSize=$ramdiskSize, osVersion=$osVersion, osPatchLevel=$osPatchLevel, headerSize=$headerSize, headerVersion=$headerVersion, cmdline='$cmdline')" + } + + companion object { + internal val log = LoggerFactory.getLogger(BootHeaderV3::class.java) + const val magic = "ANDROID!" + const val FORMAT_STRING = "8s" + //"ANDROID!" + "4I" + //kernel size, ramdisk size, os_version/patch, header size + "4I" + //reserved + "I" + //header version + "1536s" //cmdline + private const val BOOT_IMAGE_HEADER_V3_SIZE = 1580 + val pageSize: UInt = 4096U + + init { + assert(BOOT_IMAGE_HEADER_V3_SIZE == Struct3(FORMAT_STRING).calcSize()) { + "internal error: expected size $BOOT_IMAGE_HEADER_V3_SIZE " + } + } + } +} diff --git a/bbootimg/src/main/kotlin/bootimg/v3/BootV3.kt b/bbootimg/src/main/kotlin/bootimg/v3/BootV3.kt new file mode 100644 index 0000000..f1fab21 --- /dev/null +++ b/bbootimg/src/main/kotlin/bootimg/v3/BootV3.kt @@ -0,0 +1,231 @@ +package cfig.bootimg.v3 + +import cfig.Avb +import cfig.Helper +import cfig.bootimg.Common.Companion.deleleIfExists +import cfig.bootimg.Common.Companion.getPaddingSize +import cfig.bootimg.Signer +import cfig.packable.VBMetaParser +import com.fasterxml.jackson.databind.ObjectMapper +import de.vandermeer.asciitable.AsciiTable +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.DefaultExecutor +import org.slf4j.LoggerFactory +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder +import cfig.bootimg.Common as C + +@OptIn(ExperimentalUnsignedTypes::class) +data class BootV3(var info: MiscInfo = MiscInfo(), + var kernel: CommArgs = CommArgs(), + val ramdisk: CommArgs = CommArgs() +) { + companion object { + private val log = LoggerFactory.getLogger(BootV3::class.java) + private val workDir = Helper.prop("workDir") + + fun parse(fileName: String): BootV3 { + val ret = BootV3() + FileInputStream(fileName).use { fis -> + val header = BootHeaderV3(fis) + //info + ret.info.output = File(fileName).name + ret.info.json = File(fileName).name.removeSuffix(".img") + ".json" + ret.info.cmdline = header.cmdline + ret.info.headerSize = header.headerSize + ret.info.headerVersion = header.headerVersion + ret.info.osVersion = header.osVersion + ret.info.osPatchLevel = header.osPatchLevel + ret.info.pageSize = BootHeaderV3.pageSize + //kernel + ret.kernel.file = workDir + "kernel" + ret.kernel.size = header.kernelSize.toInt() + ret.kernel.position = BootHeaderV3.pageSize.toInt() + //ramdisk + ret.ramdisk.file = workDir + "ramdisk.img.gz" + ret.ramdisk.size = header.ramdiskSize.toInt() + ret.ramdisk.position = ret.kernel.position + header.kernelSize.toInt() + + getPaddingSize(header.kernelSize, BootHeaderV3.pageSize).toInt() + } + ret.info.imageSize = File(fileName).length() + return ret + } + } + + data class MiscInfo( + var output: String = "", + var json: String = "", + var headerVersion: UInt = 0U, + var headerSize: UInt = 0U, + var pageSize: UInt = 0U, + var cmdline: String = "", + var osVersion: String = "", + var osPatchLevel: String = "", + var imageSize: Long = 0 + ) + + data class CommArgs( + var file: String = "", + var position: Int = 0, + var size: Int = 0) + + fun pack(): BootV3 { + if (File( this.ramdisk.file).exists() && !File(workDir + "root").exists()) { + //do nothing if we have ramdisk.img.gz but no /root + log.warn("Use prebuilt ramdisk file: ${this.ramdisk.file}") + } else { + File(this.ramdisk.file).deleleIfExists() + File(this.ramdisk.file.removeSuffix(".gz")).deleleIfExists() + C.packRootfs("$workDir/root", this.ramdisk.file) + } + this.kernel.size = File(this.kernel.file).length().toInt() + this.ramdisk.size = File( this.ramdisk.file).length().toInt() + + //header + FileOutputStream(this.info.output + ".clear", false).use { fos -> + val encodedHeader = this.toHeader().encode() + fos.write(encodedHeader) + fos.write(ByteArray(( + Helper.round_to_multiple(encodedHeader.size.toUInt(), + this.info.pageSize) - encodedHeader.size.toUInt()).toInt() + )) + } + + //data + log.info("Writing data ...") + val bf = ByteBuffer.allocate(1024 * 1024 * 64)//assume total SIZE small than 64MB + bf.order(ByteOrder.LITTLE_ENDIAN) + C.writePaddedFile(bf, this.kernel.file, this.info.pageSize) + C.writePaddedFile(bf, this.ramdisk.file, this.info.pageSize) + //write + FileOutputStream("${this.info.output}.clear", true).use { fos -> + fos.write(bf.array(), 0, bf.position()) + } + + //google way + this.toCommandLine().addArgument(this.info.output + ".google").let { + log.info(it.toString()) + DefaultExecutor().execute(it) + } + + C.assertFileEquals(this.info.output + ".clear", this.info.output + ".google") + return this + } + + fun sign(fileName: String): BootV3 { + Signer.signAVB(fileName, this.info.imageSize) + return this + } + + private fun toHeader(): BootHeaderV3 { + return BootHeaderV3( + kernelSize = kernel.size.toUInt(), + ramdiskSize = ramdisk.size.toUInt(), + headerVersion = info.headerVersion, + osVersion = info.osVersion, + osPatchLevel = info.osPatchLevel, + headerSize = info.headerSize, + cmdline = info.cmdline) + } + + fun extractImages(): BootV3 { + val workDir = Helper.prop("workDir") + //info + ObjectMapper().writerWithDefaultPrettyPrinter().writeValue(File(workDir + this.info.json), this) + //kernel + C.dumpKernel(C.Slice(info.output, kernel.position, kernel.size, kernel.file)) + //ramdisk + C.dumpRamdisk(C.Slice(info.output, ramdisk.position, ramdisk.size, ramdisk.file), "${workDir}root") + return this + } + + fun extractVBMeta(): BootV3 { + Avb().parseVbMeta(info.output) + if (File("vbmeta.img").exists()) { + log.warn("Found vbmeta.img, parsing ...") + VBMetaParser().unpack("vbmeta.img") + } + return this + } + + fun printSummary(): BootV3 { + val workDir = Helper.prop("workDir") + val tableHeader = AsciiTable().apply { + addRule() + addRow("What", "Where") + addRule() + } + val tab = AsciiTable().let { + it.addRule() + it.addRow("image info", workDir + info.output.removeSuffix(".img") + ".json") + it.addRule() + it.addRow("kernel", this.kernel.file) + File(Helper.prop("kernelVersionFile")).let { kernelVersionFile -> + if (kernelVersionFile.exists()) { + it.addRow("\\-- version " + kernelVersionFile.readLines().toString(), kernelVersionFile.path) + } + } + File(Helper.prop("kernelConfigFile")).let { kernelConfigFile -> + if (kernelConfigFile.exists()) { + it.addRow("\\-- config", kernelConfigFile.path) + } + } + it.addRule() + it.addRow("ramdisk", this.ramdisk.file) + it.addRow("\\-- extracted ramdisk rootfs", "${workDir}root") + it.addRule() + it.addRow("AVB info", Avb.getJsonFileName(info.output)) + it.addRule() + it + } + val tabVBMeta = AsciiTable().let { + if (File("vbmeta.img").exists()) { + it.addRule() + it.addRow("vbmeta.img", Avb.getJsonFileName("vbmeta.img")) + it.addRule() + "\n" + it.render() + } else { + "" + } + } + log.info("\n\t\t\tUnpack Summary of ${info.output}\n{}\n{}{}", + tableHeader.render(), tab.render(), tabVBMeta) + return this + } + + private fun toCommandLine(): CommandLine { + return CommandLine(Helper.prop("mkbootimg")).let { ret -> + ret.addArgument("--header_version") + ret.addArgument(info.headerVersion.toString()) + if (kernel.size > 0) { + ret.addArgument("--kernel") + ret.addArgument(this.kernel.file) + } + if (ramdisk.size > 0) { + ret.addArgument("--ramdisk") + ret.addArgument(this.ramdisk.file) + } + if (info.cmdline.isNotBlank()) { + ret.addArgument(" --cmdline ") + ret.addArgument(info.cmdline, false) + } + if (info.osVersion.isNotBlank()) { + ret.addArgument(" --os_version") + ret.addArgument(info.osVersion) + } + if (info.osPatchLevel.isNotBlank()) { + ret.addArgument(" --os_patch_level") + ret.addArgument(info.osPatchLevel) + } + ret.addArgument(" --id ") + ret.addArgument(" --output ") + //ret.addArgument("boot.img" + ".google") + + log.debug("To Commandline: $ret") + ret + } + } +} diff --git a/bbootimg/src/main/kotlin/bootimg/v3/VendorBoot.kt b/bbootimg/src/main/kotlin/bootimg/v3/VendorBoot.kt new file mode 100644 index 0000000..cfe413e --- /dev/null +++ b/bbootimg/src/main/kotlin/bootimg/v3/VendorBoot.kt @@ -0,0 +1,213 @@ +package cfig.bootimg.v3 + +import cfig.Avb +import cfig.Helper +import cfig.bootimg.Common.Companion.deleleIfExists +import cfig.bootimg.Signer +import cfig.packable.VBMetaParser +import com.fasterxml.jackson.databind.ObjectMapper +import de.vandermeer.asciitable.AsciiTable +import org.apache.commons.exec.CommandLine +import org.apache.commons.exec.DefaultExecutor +import org.slf4j.LoggerFactory +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder +import cfig.bootimg.Common as C + +@OptIn(ExperimentalUnsignedTypes::class) +data class VendorBoot(var info: MiscInfo = MiscInfo(), + var ramdisk: CommArgs = CommArgs(), + var dtb: CommArgs = CommArgs()) { + data class CommArgs( + var file: String = "", + var position: UInt = 0U, + var size: UInt = 0U, + var loadAddr: UInt = 0U) + + data class MiscInfo( + var output: String = "", + var json: String = "", + var headerVersion: UInt = 0U, + var product: String = "", + var headerSize: UInt = 0U, + var pageSize: UInt = 0U, + var cmdline: String = "", + var tagsLoadAddr: UInt = 0U, + var kernelLoadAddr: UInt = 0U, + var imageSize: Long = 0 + ) + + companion object { + private val log = LoggerFactory.getLogger(VendorBoot::class.java) + fun parse(fileName: String): VendorBoot { + val ret = VendorBoot() + val workDir = Helper.prop("workDir") + FileInputStream(fileName).use { fis -> + val header = VendorBootHeader(fis) + ret.info.output = File(fileName).name + ret.info.json = File(fileName).name.removeSuffix(".img") + ".json" + ret.info.headerSize = header.headerSize + ret.info.product = header.product + ret.info.tagsLoadAddr = header.tagsLoadAddr + ret.info.cmdline = header.cmdline + ret.info.kernelLoadAddr = header.kernelLoadAddr + ret.info.pageSize = header.pageSize + ret.info.headerVersion = header.headerVersion + //ramdisk + ret.ramdisk.file = workDir + "ramdisk.img.gz" + ret.ramdisk.size = header.vndRamdiskSize + ret.ramdisk.loadAddr = header.ramdiskLoadAddr + ret.ramdisk.position = Helper.round_to_multiple( + VendorBootHeader.VENDOR_BOOT_IMAGE_HEADER_V3_SIZE, + header.pageSize) + //dtb + ret.dtb.file = workDir + "dtb" + ret.dtb.size = header.dtbSize + ret.dtb.loadAddr = header.dtbLoadAddr.toUInt() + ret.dtb.position = ret.ramdisk.position + + Helper.round_to_multiple(ret.ramdisk.size, header.pageSize) + } + ret.info.imageSize = File(fileName).length() + return ret + } + } + + fun pack(): VendorBoot { + val workDir = Helper.prop("workDir") + if (File(workDir + this.ramdisk.file).exists() && !File(workDir + "root").exists()) { + //do nothing if we have ramdisk.img.gz but no /root + log.warn("Use prebuilt ramdisk file: ${this.ramdisk.file}") + } else { + File(this.ramdisk.file).deleleIfExists() + File(this.ramdisk.file.removeSuffix(".gz")).deleleIfExists() + C.packRootfs("$workDir/root", this.ramdisk.file) + } + this.ramdisk.size = File(this.ramdisk.file).length().toUInt() + this.dtb.size = File(this.dtb.file).length().toUInt() + //header + FileOutputStream(this.info.output + ".clear", false).use { fos -> + val encodedHeader = this.toHeader().encode() + fos.write(encodedHeader) + fos.write(ByteArray(( + Helper.round_to_multiple(encodedHeader.size.toUInt(), + this.info.pageSize) - encodedHeader.size.toUInt()).toInt() + )) + } + //data + log.info("Writing data ...") + val bf = ByteBuffer.allocate(1024 * 1024 * 128)//assume total SIZE small than 64MB + bf.order(ByteOrder.LITTLE_ENDIAN) + C.writePaddedFile(bf, this.ramdisk.file, this.info.pageSize) + C.writePaddedFile(bf, this.dtb.file, this.info.pageSize) + //write + FileOutputStream("${this.info.output}.clear", true).use { fos -> + fos.write(bf.array(), 0, bf.position()) + } + + //google way + this.toCommandLine().addArgument(this.info.output + ".google").let { + log.info(it.toString()) + DefaultExecutor().execute(it) + } + + C.assertFileEquals(this.info.output + ".clear", this.info.output + ".google") + return this + } + + fun sign(): VendorBoot { + Signer.signAVB(info.output, this.info.imageSize) + return this + } + + private fun toHeader(): VendorBootHeader { + return VendorBootHeader( + headerVersion = info.headerVersion, + pageSize = info.pageSize, + kernelLoadAddr = info.kernelLoadAddr, + ramdiskLoadAddr = ramdisk.loadAddr, + vndRamdiskSize = ramdisk.size, + cmdline = info.cmdline, + tagsLoadAddr = info.tagsLoadAddr, + product = info.product, + headerSize = info.headerSize, + dtbSize = dtb.size, + dtbLoadAddr = dtb.loadAddr.toULong() + ) + } + + fun extractImages(): VendorBoot { + val workDir = Helper.prop("workDir") + //header + ObjectMapper().writerWithDefaultPrettyPrinter().writeValue( + File(workDir + this.info.json), this) + //ramdisk + C.dumpRamdisk(C.Slice(info.output, ramdisk.position.toInt(), ramdisk.size.toInt(), ramdisk.file), + "${workDir}root") + //dtb + C.dumpDtb(C.Slice(info.output, dtb.position.toInt(), dtb.size.toInt(), dtb.file)) + return this + } + + fun extractVBMeta(): VendorBoot { + Avb().parseVbMeta(info.output) + if (File("vbmeta.img").exists()) { + log.warn("Found vbmeta.img, parsing ...") + VBMetaParser().unpack("vbmeta.img") + } + return this + } + + fun printSummary(): VendorBoot { + val workDir = Helper.prop("workDir") + val tableHeader = AsciiTable().apply { + addRule() + addRow("What", "Where") + addRule() + } + val tab = AsciiTable().let { + it.addRule() + it.addRow("image info", workDir + info.output.removeSuffix(".img") + ".json") + it.addRule() + it.addRow("ramdisk", this.ramdisk.file) + it.addRow("\\-- extracted ramdisk rootfs", "${workDir}root") + it.addRule() + it.addRow("dtb", this.dtb.file) + if (File(this.dtb.file + ".src").exists()) { + it.addRow("\\-- decompiled dts", dtb.file + ".src") + } + it.addRule() + it.addRow("AVB info", Avb.getJsonFileName(info.output)) + it.addRule() + it + } + val tabVBMeta = AsciiTable().let { + if (File("vbmeta.img").exists()) { + it.addRule() + it.addRow("vbmeta.img", Avb.getJsonFileName("vbmeta.img")) + it.addRule() + "\n" + it.render() + } else { + "" + } + } + log.info("\n\t\t\tUnpack Summary of ${info.output}\n{}\n{}{}", + tableHeader.render(), tab.render(), tabVBMeta) + return this + } + + private fun toCommandLine(): CommandLine { + return CommandLine(Helper.prop("mkbootimg")) + .addArgument("--vendor_ramdisk") + .addArgument(this.ramdisk.file) + .addArgument("--dtb") + .addArgument(this.dtb.file) + .addArgument("--vendor_cmdline") + .addArgument(info.cmdline, false) + .addArgument("--header_version") + .addArgument(info.headerVersion.toString()) + .addArgument("--vendor_boot") + } +} diff --git a/bbootimg/src/main/kotlin/bootimg/v3/VendorBootHeader.kt b/bbootimg/src/main/kotlin/bootimg/v3/VendorBootHeader.kt new file mode 100644 index 0000000..bb3c53e --- /dev/null +++ b/bbootimg/src/main/kotlin/bootimg/v3/VendorBootHeader.kt @@ -0,0 +1,88 @@ +package cfig.bootimg.v3 + +import cfig.io.Struct3 +import org.slf4j.LoggerFactory +import java.io.InputStream + +@OptIn(ExperimentalUnsignedTypes::class) +class VendorBootHeader( + var headerVersion: UInt = 0U, + var pageSize: UInt = 0U, + var kernelLoadAddr: UInt = 0U, + var ramdiskLoadAddr: UInt = 0U, + var vndRamdiskSize: UInt = 0U, + var cmdline: String = "", + var tagsLoadAddr: UInt = 0U, + var product: String = "", + var headerSize: UInt = 0U, + var dtbSize: UInt = 0U, + var dtbLoadAddr: ULong = 0U +) { + @Throws(IllegalArgumentException::class) + constructor(iS: InputStream?) : this() { + if (iS == null) { + return + } + log.warn("VendorBootHeader constructor") + val info = Struct3(FORMAT_STRING).unpack(iS) + assert(12 == info.size) + if (info[0] != magic) { + throw IllegalArgumentException("stream doesn't look like Android Vendor Boot Image") + } + this.headerVersion = info[1] as UInt + this.pageSize = info[2] as UInt + this.kernelLoadAddr = info[3] as UInt + this.ramdiskLoadAddr = info[4] as UInt + this.vndRamdiskSize = info[5] as UInt + this.cmdline = info[6] as String + this.tagsLoadAddr = info[7] as UInt + this.product = info[8] as String + this.headerSize = info[9] as UInt + this.dtbSize = info[10] as UInt + this.dtbLoadAddr = info[11] as ULong + + assert(this.headerSize in arrayOf(VENDOR_BOOT_IMAGE_HEADER_V3_SIZE)) + assert(this.headerVersion == 3U) + } + + fun encode(): ByteArray { + return Struct3(FORMAT_STRING).pack( + magic, + headerVersion, + pageSize, + kernelLoadAddr, + ramdiskLoadAddr, + vndRamdiskSize, + cmdline, + tagsLoadAddr, + product, + headerSize, + dtbSize, + dtbLoadAddr) + } + + companion object { + private val log = LoggerFactory.getLogger(VendorBootHeader::class.java) + const val magic = "VNDRBOOT" + const val VENDOR_BOOT_IMAGE_HEADER_V3_SIZE = 2112U + const val FORMAT_STRING = "8s" + //magic + "I" + //header version + "I" + //page size + "I" + //kernel physical load addr + "I" + //ramdisk physical load addr + "I" + //vendor ramdisk size + "2048s" + //cmdline + "I" + //kernel tag load addr + "16s" + //product name + "I" + //header size + "I" + //dtb size + "Q" //dtb physical load addr + init { + assert(Struct3(FORMAT_STRING).calcSize().toUInt() == VENDOR_BOOT_IMAGE_HEADER_V3_SIZE) + } + } + + override fun toString(): String { + return "VendorBootHeader(headerVersion=$headerVersion, pageSize=$pageSize, kernelLoadAddr=$kernelLoadAddr, ramdiskLoadAddr=$ramdiskLoadAddr, vndRamdiskSize=$vndRamdiskSize, cmdline='$cmdline', tagsLoadAddr=$tagsLoadAddr, product='$product', headerSize=$headerSize, dtbSize=$dtbSize, dtbLoadAddr=$dtbLoadAddr)" + } +} diff --git a/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt b/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt index 6f8f531..5eadcb6 100644 --- a/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt +++ b/bbootimg/src/main/kotlin/kernel_util/KernelExtractor.kt @@ -1,7 +1,7 @@ package cfig.kernel_util import cfig.EnvironmentVerifier -import cfig.InfoTable +import cfig.Helper import org.apache.commons.exec.CommandLine import org.apache.commons.exec.DefaultExecutor import org.slf4j.Logger @@ -16,11 +16,11 @@ class KernelExtractor { return envv.hasLz4 && envv.hasXz && envv.hasGzip } - fun run(fileName: String, workDir: File? = null) { - val baseDir = "build/unzip_boot" - val kernelVersionFile = "$baseDir/kernel_version.txt" - val kernelConfigFile = "$baseDir/kernel_configs.txt" - val cmd = CommandLine.parse("aosp/build/tools/extract_kernel.py").let { + fun run(fileName: String, workDir: File? = null): List { + val ret: MutableList = mutableListOf() + val kernelVersionFile = Helper.prop("kernelVersionFile") + val kernelConfigFile = Helper.prop("kernelConfigFile") + val cmd = CommandLine.parse(Helper.prop("kernelExtracter")).let { it.addArgument("--input") it.addArgument(fileName) it.addArgument("--output-configs") @@ -36,11 +36,13 @@ class KernelExtractor { val kernelVersion = File(kernelVersionFile).readLines() log.info("kernel version: $kernelVersion") log.info("kernel config dumped to : $kernelConfigFile") - InfoTable.instance.addRow("\\-- version $kernelVersion", kernelVersionFile) - InfoTable.instance.addRow("\\-- config", kernelConfigFile) + ret.add(kernelVersion.toString()) + ret.add(kernelVersionFile) + ret.add(kernelConfigFile) } catch (e: org.apache.commons.exec.ExecuteException) { log.warn("can not parse kernel info") } } + return ret } } diff --git a/bbootimg/src/main/kotlin/packable/BootImgParser.kt b/bbootimg/src/main/kotlin/packable/BootImgParser.kt index c0e0c39..7b296dc 100644 --- a/bbootimg/src/main/kotlin/packable/BootImgParser.kt +++ b/bbootimg/src/main/kotlin/packable/BootImgParser.kt @@ -2,66 +2,47 @@ package cfig.packable import avb.AVBInfo import avb.blob.Footer -import cfig.* -import cfig.bootimg.BootImgInfo +import cfig.Avb +import cfig.Helper +import cfig.bootimg.Common.Companion.probeHeaderVersion +import cfig.bootimg.v2.BootV2 +import cfig.bootimg.v3.BootV3 import com.fasterxml.jackson.databind.ObjectMapper -import de.vandermeer.asciitable.AsciiTable import org.slf4j.LoggerFactory import java.io.File import java.io.FileInputStream -import java.lang.IllegalArgumentException @OptIn(ExperimentalUnsignedTypes::class) class BootImgParser() : IPackable { override val loopNo: Int get() = 0 - private val log = LoggerFactory.getLogger(BootImgParser::class.java) + private val workDir = Helper.prop("workDir") override fun capabilities(): List { return listOf("^boot\\.img$", "^recovery\\.img$", "^recovery-two-step\\.img$") } - private fun unpackVBMeta(): Boolean { - return if (File("vbmeta.img").exists()) { - log.warn("Found vbmeta.img, parsing ...") - VBMetaParser().unpack("vbmeta.img") - true - } else { - false - } - } - override fun unpack(fileName: String) { cleanUp() try { - val info = Parser().parseBootImgHeader(fileName, avbtool = "aosp/avb/avbtool") - InfoTable.instance.addRule() - InfoTable.instance.addRow("image info", ParamConfig().cfg) - if (info.signatureType == BootImgInfo.VerifyType.AVB) { - log.info("continue to analyze vbmeta info in $fileName") - Avb().parseVbMeta(fileName) - InfoTable.instance.addRule() - InfoTable.instance.addRow("AVB info", Avb.getJsonFileName(fileName)) - } - Parser().extractBootImg(fileName, info2 = info) - val unpackedVbmeta = unpackVBMeta() - - InfoTable.instance.addRule() - val tableHeader = AsciiTable().apply { - addRule() - addRow("What", "Where") - addRule() + val hv = probeHeaderVersion(fileName) + log.info("header version $hv") + if (hv == 3) { + val b3 = BootV3 + .parse(fileName) + .extractImages() + .extractVBMeta() + .printSummary() + log.debug(b3.toString()) + return + } else { + val b2 = BootV2 + .parse(fileName) + .extractImages() + .extractVBMeta() + .printSummary() + log.debug(b2.toString()) } - log.info("\n\t\t\tUnpack Summary of $fileName\n{}\n{}", tableHeader.render(), InfoTable.instance.render()) - if (unpackedVbmeta) { - val tableFooter = AsciiTable().apply { - addRule() - addRow("vbmeta.img", Avb.getJsonFileName("vbmeta.img")) - addRule() - } - LoggerFactory.getLogger("vbmeta").info("\n" + tableFooter.render()) - } - log.info("Following components are not present: ${InfoTable.missingParts}") } catch (e: IllegalArgumentException) { log.error(e.message) log.error("Parser can not continue") @@ -69,35 +50,28 @@ class BootImgParser() : IPackable { } override fun pack(fileName: String) { - Packer().pack(mkbootfsBin = "./aosp/mkbootfs/build/exe/mkbootfs/mkbootfs") - Signer.sign(avbtool = "aosp/avb/avbtool", bootSigner = "aosp/boot_signer/build/libs/boot_signer.jar") - if (File("vbmeta.img").exists()) { - val partitionName = ObjectMapper().readValue(File(Avb.getJsonFileName(fileName)), AVBInfo::class.java).let { - it.auxBlob!!.hashDescriptors.get(0).partition_name - } - val newHashDesc = Avb().parseVbMeta("$fileName.signed", dumpFile = false) - assert(newHashDesc.auxBlob!!.hashDescriptors.size == 1) - val mainVBMeta = ObjectMapper().readValue(File(Avb.getJsonFileName("vbmeta.img")), AVBInfo::class.java).apply { - val itr = this.auxBlob!!.hashDescriptors.iterator() - var seq = 0 - while (itr.hasNext()) { - val itrValue = itr.next() - if (itrValue.partition_name == partitionName) { - seq = itrValue.sequence - itr.remove() - break - } - } - val hd = newHashDesc.auxBlob!!.hashDescriptors.get(0).apply { this.sequence = seq } - this.auxBlob!!.hashDescriptors.add(hd) - } - Avb().packVbMetaWithPadding("vbmeta.img", mainVBMeta) + val cfgFile = workDir + fileName.removeSuffix(".img") + ".json" + log.info("Loading config from $cfgFile") + if (3 == probeHeaderVersion(fileName)) { + ObjectMapper().readValue(File(cfgFile), BootV3::class.java) + .pack() + .sign(fileName) + updateVbmeta(fileName) + } else { + ObjectMapper().readValue(File(cfgFile), BootV2::class.java) + .pack() + .sign() + updateVbmeta(fileName) } } override fun flash(fileName: String, deviceName: String) { val stem = fileName.substring(0, fileName.indexOf(".")) super.flash("$fileName.signed", stem) + + if (File("vbmeta.img.signed").exists()) { + super.flash("vbmeta.img.signed", "vbmeta") + } } // invoked solely by reflection @@ -112,4 +86,35 @@ class BootImgParser() : IPackable { } } } + + companion object { + private val log = LoggerFactory.getLogger(BootImgParser::class.java) + + fun updateVbmeta(fileName: String) { + log.info("Updating vbmeta.img side by side ...") + if (File("vbmeta.img").exists()) { + val partitionName = ObjectMapper().readValue(File(Avb.getJsonFileName(fileName)), AVBInfo::class.java).let { + it.auxBlob!!.hashDescriptors.get(0).partition_name + } + val newHashDesc = Avb().parseVbMeta("$fileName.signed", dumpFile = false) + assert(newHashDesc.auxBlob!!.hashDescriptors.size == 1) + val mainVBMeta = ObjectMapper().readValue(File(Avb.getJsonFileName("vbmeta.img")), AVBInfo::class.java).apply { + val itr = this.auxBlob!!.hashDescriptors.iterator() + var seq = 0 + while (itr.hasNext()) { + val itrValue = itr.next() + if (itrValue.partition_name == partitionName) { + log.info("Found $partitionName in vbmeta, update it") + seq = itrValue.sequence + itr.remove() + break + } + } + val hd = newHashDesc.auxBlob!!.hashDescriptors.get(0).apply { this.sequence = seq } + this.auxBlob!!.hashDescriptors.add(hd) + } + Avb().packVbMetaWithPadding("vbmeta.img", mainVBMeta) + } + } + } } diff --git a/bbootimg/src/main/kotlin/packable/DtboParser.kt b/bbootimg/src/main/kotlin/packable/DtboParser.kt index bdafeaa..b5575ad 100644 --- a/bbootimg/src/main/kotlin/packable/DtboParser.kt +++ b/bbootimg/src/main/kotlin/packable/DtboParser.kt @@ -1,7 +1,7 @@ package cfig.packable import cfig.EnvironmentVerifier -import cfig.UnifiedConfig +import cfig.Helper import cfig.dtb_util.DTC import org.apache.commons.exec.CommandLine import org.apache.commons.exec.DefaultExecutor @@ -19,6 +19,7 @@ class DtboParser(val workDir: File) : IPackable { private val log = LoggerFactory.getLogger(DtboParser::class.java) private val envv = EnvironmentVerifier() + private val outDir = Helper.prop("workDir") override fun capabilities(): List { return listOf("^dtbo\\.img$") @@ -26,9 +27,8 @@ class DtboParser(val workDir: File) : IPackable { override fun unpack(fileName: String) { cleanUp() - val outputDir = UnifiedConfig.workDir - val dtbPath = File("$outputDir/dtb").path!! - val headerPath = File("$outputDir/dtbo.header").path!! + val dtbPath = File("$outDir/dtb").path + val headerPath = File("$outDir/dtbo.header").path val cmd = CommandLine.parse("external/mkdtboimg.py dump $fileName").let { it.addArguments("--dtb $dtbPath") it.addArguments("--output $headerPath") @@ -40,7 +40,7 @@ class DtboParser(val workDir: File) : IPackable { if (envv.hasDtc) { for (i in 0 until Integer.parseInt(props.getProperty("dt_entry_count"))) { val inputDtb = "$dtbPath.$i" - val outputSrc = File(UnifiedConfig.workDir + "/" + File(inputDtb).name + ".src").path + val outputSrc = File(outDir + "/" + File(inputDtb).name + ".src").path DTC().decompile(inputDtb, outputSrc) } } else { @@ -54,13 +54,13 @@ class DtboParser(val workDir: File) : IPackable { return } - val headerPath = File("${UnifiedConfig.workDir}/dtbo.header").path + val headerPath = File("${outDir}/dtbo.header").path val props = Properties() props.load(FileInputStream(File(headerPath))) val cmd = CommandLine.parse("external/mkdtboimg.py create $fileName.clear").let { it.addArguments("--version=1") for (i in 0 until Integer.parseInt(props.getProperty("dt_entry_count"))) { - val dtsName = File(UnifiedConfig.workDir + "/dtb.$i").path + val dtsName = File("$outDir/dtb.$i").path it.addArguments(dtsName) } it diff --git a/bbootimg/src/main/kotlin/packable/IPackable.kt b/bbootimg/src/main/kotlin/packable/IPackable.kt index 396bee7..f8e3d06 100644 --- a/bbootimg/src/main/kotlin/packable/IPackable.kt +++ b/bbootimg/src/main/kotlin/packable/IPackable.kt @@ -1,8 +1,8 @@ package cfig.packable +import cfig.Helper import cfig.Helper.Companion.check_call import cfig.Helper.Companion.check_output -import cfig.UnifiedConfig import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File @@ -32,8 +32,9 @@ interface IPackable { } fun cleanUp() { - if (File(UnifiedConfig.workDir).exists()) File(UnifiedConfig.workDir).deleteRecursively() - File(UnifiedConfig.workDir).mkdirs() + val workDir = Helper.prop("workDir") + if (File(workDir).exists()) File(workDir).deleteRecursively() + File(workDir).mkdirs() } companion object { diff --git a/bbootimg/src/main/kotlin/packable/PackableLauncher.kt b/bbootimg/src/main/kotlin/packable/PackableLauncher.kt index 51ffe05..5851073 100644 --- a/bbootimg/src/main/kotlin/packable/PackableLauncher.kt +++ b/bbootimg/src/main/kotlin/packable/PackableLauncher.kt @@ -15,7 +15,7 @@ class PackableLauncher fun main(args: Array) { val log = LoggerFactory.getLogger(PackableLauncher::class.java) val packablePool = mutableMapOf, KClass>() - listOf(DtboParser(), VBMetaParser(), BootImgParser(), SparseImgParser()).forEach { + listOf(DtboParser(), VBMetaParser(), BootImgParser(), SparseImgParser(), VendorBootParser()).forEach { @Suppress("UNCHECKED_CAST") packablePool.put(it.capabilities(), it::class as KClass) } @@ -78,8 +78,8 @@ fun main(args: Array) { if (functions.size != 1) { log.error("command '${args[0]}' can not be recognized") log.info("available ${it.simpleName} subcommands are:") - it.declaredFunctions.forEach { - log.info("\t" + it.name) + it.declaredFunctions.forEach { theFunc -> + log.info("\t" + theFunc.name) } exitProcess(3) } @@ -91,6 +91,15 @@ fun main(args: Array) { 2 -> { functions[0].call(it.createInstance(), targetFile!!) } + 3 -> { + if (args.size != 2 ) { + log.info("invoke: ${it.qualifiedName}, $targetFile, " + targetFile!!.removeSuffix(".img")) + functions[0].call(it.createInstance(), targetFile!!, targetFile!!.removeSuffix(".img")) + } else { + log.info("invoke: ${it.qualifiedName}, $targetFile, " + args[1]) + functions[0].call(it.createInstance(), targetFile!!, args[1]) + } + } else -> { functions[0].parameters.forEach { kp -> println("Param: " + kp.index + " " + kp.type + " " + kp.name) diff --git a/bbootimg/src/main/kotlin/packable/VBMetaParser.kt b/bbootimg/src/main/kotlin/packable/VBMetaParser.kt index 2556ba9..99f1979 100644 --- a/bbootimg/src/main/kotlin/packable/VBMetaParser.kt +++ b/bbootimg/src/main/kotlin/packable/VBMetaParser.kt @@ -1,7 +1,7 @@ package cfig.packable import cfig.Avb -import cfig.UnifiedConfig +import cfig.Helper import java.io.File @OptIn(ExperimentalUnsignedTypes::class) @@ -14,7 +14,7 @@ class VBMetaParser: IPackable { } override fun cleanUp() { - File(UnifiedConfig.workDir).mkdirs() + File(Helper.prop("workDir")).mkdirs() } override fun unpack(fileName: String) { diff --git a/bbootimg/src/main/kotlin/packable/VendorBootParser.kt b/bbootimg/src/main/kotlin/packable/VendorBootParser.kt new file mode 100644 index 0000000..ee7930c --- /dev/null +++ b/bbootimg/src/main/kotlin/packable/VendorBootParser.kt @@ -0,0 +1,36 @@ +package cfig.packable + +import cfig.Helper +import cfig.bootimg.v3.VendorBoot +import cfig.packable.BootImgParser.Companion.updateVbmeta +import com.fasterxml.jackson.databind.ObjectMapper +import org.slf4j.LoggerFactory +import java.io.File + +class VendorBootParser : IPackable { + override val loopNo: Int = 0 + private val log = LoggerFactory.getLogger(VendorBootParser::class.java) + private val workDir = Helper.prop("workDir") + override fun capabilities(): List { + return listOf("^vendor_boot\\.img$") + } + + override fun unpack(fileName: String) { + cleanUp() + val vb = VendorBoot + .parse(fileName) + .extractImages() + .extractVBMeta() + .printSummary() + log.debug(vb.toString()) + } + + override fun pack(fileName: String) { + val cfgFile = "$workDir/${fileName.removeSuffix(".img")}.json" + log.info("Loading config from $cfgFile") + ObjectMapper().readValue(File(cfgFile), VendorBoot::class.java) + .pack() + .sign() + updateVbmeta(fileName) + } +} diff --git a/bbootimg/src/main/resources/general.cfg b/bbootimg/src/main/resources/general.cfg new file mode 100644 index 0000000..458aaf3 --- /dev/null +++ b/bbootimg/src/main/resources/general.cfg @@ -0,0 +1,10 @@ +workDir = build/unzip_boot/ +mkbootfsBin = aosp/mkbootfs/build/exe/mkbootfs/mkbootfs +avbtool = aosp/avb/avbtool +kernelExtracter = aosp/build/tools/extract_kernel.py +bootSigner = aosp/boot_signer/build/libs/boot_signer.jar +verity_pk8 = aosp/security/verity.pk8 +verity_pem = aosp/security/verity.x509.pem +mkbootimg = aosp/system/tools/mkbootimg/mkbootimg.py +kernelVersionFile = build/unzip_boot/kernel_version.txt +kernelConfigFile = build/unzip_boot/kernel_configs.txt diff --git a/build.gradle.kts b/build.gradle.kts index fae7552..0ef61f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,7 @@ tasks { main = "cfig.packable.PackableLauncherKt" classpath = files("bbootimg/build/libs/bbootimg.jar") this.maxHeapSize = "512m" + enableAssertions = true args("unpack") } unpackTask.dependsOn("bbootimg:jar") @@ -45,6 +46,7 @@ tasks { main = "cfig.packable.PackableLauncherKt" classpath = files("bbootimg/build/libs/bbootimg.jar") this.maxHeapSize = "512m" + enableAssertions = true args("pack") } packTask.dependsOn("bbootimg:jar", "aosp:boot_signer:build") @@ -54,6 +56,7 @@ tasks { main = "cfig.packable.PackableLauncherKt" classpath = files("bbootimg/build/libs/bbootimg.jar") this.maxHeapSize = "512m" + enableAssertions = true args("flash") } flashTask.dependsOn("bbootimg:jar") diff --git a/doc/layout.md b/doc/layout.md index b553866..797d289 100644 --- a/doc/layout.md +++ b/doc/layout.md @@ -1,8 +1,11 @@ -# layout of boot.img +# layout of [vendor\_]boot.img -### Image Content Index +## Image Content Index [1 header part](#1-header-part) + - [1.1 boot.img v0-v2](#11-bootimg-header-v0-v2) + - [1.2 boot.img v3](#12-bootimg-header-v3) + - [1.3 vendor\_boot.img v3](#13-vendor_bootimg-header-v3) [2 data part](#2-data-part) @@ -12,11 +15,13 @@ - [3.2 AVB Footer](#32-avb-footer-vboot-20) -### 1. header part +## 1. header part +### 1.1 boot.img header v0-v2 +value at 0x28 is 0x00,0x01,0x02 item size in bytes position +-----------------------------------------------------------+ --> 0 - | | 8 | + | | 8 (value=ANDROID!) | |--------------------------------+--------------------------| --> 8 | | 4 | |--------------------------------+--------------------------| --> 12 @@ -61,7 +66,63 @@ | | - header_size) | +--------------------------------+--------------------------+ --> pagesize -### 2. data part +### 1.2 boot.img header v3 + + item size in bytes position + +-----------------------------------------------------------+ --> 0 + | | 8 (value=ANDROID!) | + |--------------------------------+--------------------------| --> 8 + | | 4 | + |--------------------------------+--------------------------| --> 12 + | | 4 | + |--------------------------------+--------------------------| --> 16 + | | 4 | + |--------------------------------+--------------------------| --> 20 + |
| 4 | + |--------------------------------+--------------------------| --> 24 + | | 4 * 4 | + |--------------------------------+--------------------------| --> 40 + |
| 4 (value=3) | + |--------------------------------+--------------------------| --> 44 + | | 1024+512=1536 | + |--------------------------------+--------------------------| --> 1580 + | | min(n * page_size | + | | - header_size) | + +--------------------------------+--------------------------+ --> pagesize=4096 + +### 1.3 vendor\_boot.img header v3 + + item size in bytes position + +-----------------------------------------------------------+ --> 0 + | | 8 (vaue=VNDRBOOT) | + |--------------------------------+--------------------------| --> 8 + |
| 4 (value=3) | + |--------------------------------+--------------------------| --> 12 + | | 4 | + |--------------------------------+--------------------------| --> 16 + | | 4 | + |--------------------------------+--------------------------| --> 20 + | | 4 | + |--------------------------------+--------------------------| --> 24 + | | 4 | + |--------------------------------+--------------------------| --> 28 + | | 2048 | + |--------------------------------+--------------------------| --> 2076 + | | 4 | + |--------------------------------+--------------------------| --> 2080 + | | 16 | + |--------------------------------+--------------------------| --> 2096 + |
| 4 (value=2112) | + |--------------------------------+--------------------------| --> 2100 + | | 4 | + |--------------------------------+--------------------------| --> 2104 + | | 8 | + |--------------------------------+--------------------------| --> 2112 + | | min(n * page_size | + | | - header_size) | + +--------------------------------+--------------------------+ --> pagesize + +## 2. data part +-----------------------------------------------------------+ --> pagesize | | kernel length | @@ -93,9 +154,9 @@ | [v2] | min(n * page_size - len) | +-----------------------------------------------------------+ --> end of data part -### 3. signature part +## 3. signature part -#### 3.1 Boot Image Signature (VBoot 1.0) +### 3.1 Boot Image Signature (VBoot 1.0) +--------------------------------+--------------------------+ --> end of data part | | signature length | @@ -103,7 +164,7 @@ | | defined by boot_signer | +--------------------------------+--------------------------+ -#### 3.2 AVB Footer (VBoot 2.0) +### 3.2 AVB Footer (VBoot 2.0) item size in bytes position +------+--------------------------------+-------------------------+ --> end of data part (say locaton +0) diff --git a/src/integrationTest/resources b/src/integrationTest/resources index d8168a2..7cd84fb 160000 --- a/src/integrationTest/resources +++ b/src/integrationTest/resources @@ -1 +1 @@ -Subproject commit d8168a226d055f8372e55e3c0096d6dca70b8611 +Subproject commit 7cd84fb4f31151410d413d138e8ae1c35cd40dd9