initial GKI images support

- boot.img header v3
 - vendor_boot.img
pull/41/head
cfig 5 years ago
parent 8bda017e85
commit 716e8363ff
No known key found for this signature in database
GPG Key ID: B104C307F0FDABB7

@ -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/

@ -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))

@ -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) {

@ -1,8 +0,0 @@
package cfig
import de.vandermeer.asciitable.AsciiTable
object InfoTable {
val instance = AsciiTable()
val missingParts = mutableListOf<String>()
}

@ -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")

@ -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 }"
}
}
}

@ -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()
}
}
}

@ -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\"")
}
}
}
}

@ -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<Long> = mutableSetOf<Long>().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())
}
}
}

@ -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
}
}

@ -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<String> {
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)
}
}
}
}

@ -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")
}

@ -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")
}
}
}

@ -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\"")
}
}
}
}

@ -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 }"
}
}
}

@ -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<Long> = mutableSetOf<Long>().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())
}
}
}

@ -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
}
}

@ -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 "
}
}
}
}

@ -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
}
}
}

@ -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")
}
}

@ -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)"
}
}

@ -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<String> {
val ret: MutableList<String> = 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
}
}

@ -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<String> {
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)
}
}
}
}

@ -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<String> {
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

@ -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 {

@ -15,7 +15,7 @@ class PackableLauncher
fun main(args: Array<String>) {
val log = LoggerFactory.getLogger(PackableLauncher::class.java)
val packablePool = mutableMapOf<List<String>, KClass<IPackable>>()
listOf(DtboParser(), VBMetaParser(), BootImgParser(), SparseImgParser()).forEach {
listOf(DtboParser(), VBMetaParser(), BootImgParser(), SparseImgParser(), VendorBootParser()).forEach {
@Suppress("UNCHECKED_CAST")
packablePool.put(it.capabilities(), it::class as KClass<IPackable>)
}
@ -78,8 +78,8 @@ fun main(args: Array<String>) {
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<String>) {
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)

@ -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) {

@ -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<String> {
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)
}
}

@ -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

@ -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")

@ -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
|<MAGIC HEADER> | 8 |
|<MAGIC HEADER> | 8 (value=ANDROID!) |
|--------------------------------+--------------------------| --> 8
|<kernel length> | 4 |
|--------------------------------+--------------------------| --> 12
@ -61,7 +66,63 @@
| | - header_size) |
+--------------------------------+--------------------------+ --> pagesize
### 2. data part
### 1.2 boot.img header v3
item size in bytes position
+-----------------------------------------------------------+ --> 0
|<MAGIC HEADER> | 8 (value=ANDROID!) |
|--------------------------------+--------------------------| --> 8
|<kernel size> | 4 |
|--------------------------------+--------------------------| --> 12
|<ramdisk size> | 4 |
|--------------------------------+--------------------------| --> 16
|<os version & os patch level> | 4 |
|--------------------------------+--------------------------| --> 20
|<header size> | 4 |
|--------------------------------+--------------------------| --> 24
|<reserved> | 4 * 4 |
|--------------------------------+--------------------------| --> 40
|<header version> | 4 (value=3) |
|--------------------------------+--------------------------| --> 44
|<cmdline> | 1024+512=1536 |
|--------------------------------+--------------------------| --> 1580
|<padding> | min(n * page_size |
| | - header_size) |
+--------------------------------+--------------------------+ --> pagesize=4096
### 1.3 vendor\_boot.img header v3
item size in bytes position
+-----------------------------------------------------------+ --> 0
|<MAGIC HEADER> | 8 (vaue=VNDRBOOT) |
|--------------------------------+--------------------------| --> 8
|<header version> | 4 (value=3) |
|--------------------------------+--------------------------| --> 12
|<page size> | 4 |
|--------------------------------+--------------------------| --> 16
|<kernel load addr> | 4 |
|--------------------------------+--------------------------| --> 20
|<ramdisk load addr> | 4 |
|--------------------------------+--------------------------| --> 24
|<vendor ramdisk size> | 4 |
|--------------------------------+--------------------------| --> 28
|<vendor cmdline> | 2048 |
|--------------------------------+--------------------------| --> 2076
|<tags offset> | 4 |
|--------------------------------+--------------------------| --> 2080
|<board name> | 16 |
|--------------------------------+--------------------------| --> 2096
|<header size> | 4 (value=2112) |
|--------------------------------+--------------------------| --> 2100
|<dtb size> | 4 |
|--------------------------------+--------------------------| --> 2104
|<dtb load addr> | 8 |
|--------------------------------+--------------------------| --> 2112
|<padding> | min(n * page_size |
| | - header_size) |
+--------------------------------+--------------------------+ --> pagesize
## 2. data part
+-----------------------------------------------------------+ --> pagesize
|<kernel> | kernel length |
@ -93,9 +154,9 @@
|<padding> [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> | signature length |
@ -103,7 +164,7 @@
|<padding> | 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)

@ -1 +1 @@
Subproject commit d8168a226d055f8372e55e3c0096d6dca70b8611
Subproject commit 7cd84fb4f31151410d413d138e8ae1c35cd40dd9
Loading…
Cancel
Save