You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Android_boot_image_editor/bbootimg/src/main/kotlin/Helper.kt

269 lines
11 KiB
Kotlin

package cfig
import avb.alg.Algorithms
import cfig.io.Struct
import com.google.common.math.BigIntegerMath
import org.apache.commons.codec.binary.Hex
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
import org.apache.commons.compress.compressors.gzip.GzipParameters
import org.apache.commons.exec.CommandLine
import org.apache.commons.exec.DefaultExecutor
import org.apache.commons.exec.ExecuteException
import org.apache.commons.exec.PumpStreamHandler
import org.bouncycastle.asn1.pkcs.RSAPrivateKey
import org.bouncycastle.util.io.pem.PemReader
import org.junit.Assert
import org.junit.Assert.assertTrue
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.security.KeyFactory
import java.security.PrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
class Helper {
companion object {
fun join(vararg source: ByteArray): ByteArray {
val baos = ByteArrayOutputStream()
for (src in source) {
if (source.isNotEmpty()) baos.write(src)
}
return baos.toByteArray()
}
fun toHexString(inData: ByteArray): String {
val sb = StringBuilder()
for (i in inData.indices) {
sb.append(Integer.toString((inData[i].toInt().and(0xff)) + 0x100, 16).substring(1))
}
return sb.toString()
}
fun fromHexString(s: String): ByteArray {
val len = s.length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
i += 2
}
return data
}
//similar to this.toString(StandardCharsets.UTF_8).replace("${Character.MIN_VALUE}", "")
fun toCString(ba: ByteArray): String {
val str = ba.toString(StandardCharsets.UTF_8)
val nullPos = str.indexOf(Character.MIN_VALUE)
return if (nullPos >= 0) {
str.substring(0, nullPos)
} else {
str
}
}
@Throws(IOException::class)
fun gnuZipFile(compressedFile: String, decompressedFile: String) {
val buffer = ByteArray(1024)
FileOutputStream(compressedFile).use { fos ->
GZIPOutputStream(fos).use { gos ->
FileInputStream(decompressedFile).use { fis ->
var bytesRead: Int
while (true) {
bytesRead = fis.read(buffer)
if (bytesRead <= 0) break
gos.write(buffer, 0, bytesRead);
}
gos.finish()
log.info("gzip done: $decompressedFile -> $compressedFile")
}//file-input-stream
}//gzip-output-stream
}//file-output-stream
}
@Throws(IOException::class)
fun unGnuzipFile(compressedFile: String, decompressedFile: String) {
val buffer = ByteArray(1024)
FileInputStream(compressedFile).use { fileIn ->
//src
GZIPInputStream(fileIn).use { gZIPInputStream ->
//src
FileOutputStream(decompressedFile).use { fileOutputStream ->
var bytesRead: Int
while (true) {
bytesRead = gZIPInputStream.read(buffer)
if (bytesRead <= 0) break
fileOutputStream.write(buffer, 0, bytesRead)
}
log.info("decompress(gz) done: $compressedFile -> $decompressedFile")
}
}
}
}
/*
caution: about gzip header - OS (Operating System)
According to https://docs.oracle.com/javase/8/docs/api/java/util/zip/package-summary.html and
GZIP spec RFC-1952(http://www.ietf.org/rfc/rfc1952.txt), gzip files created from java.util.zip.GZIPOutputStream
will mark the OS field with
0 - FAT filesystem (MS-DOS, OS/2, NT/Win32)
But default image built from Android source code has the OS field:
3 - Unix
This MAY not be a problem, at least we didn't find it till now.
*/
@Throws(IOException::class)
fun gnuZipFile(compressedFile: String, fis: InputStream) {
val buffer = ByteArray(1024)
FileOutputStream(compressedFile).use { fos ->
GZIPOutputStream(fos).use { gos ->
var bytesRead: Int
while (true) {
bytesRead = fis.read(buffer)
if (bytesRead <= 0) break
gos.write(buffer, 0, bytesRead)
}
log.info("compress(gz) done: $compressedFile")
}
}
}
fun gnuZipFile2(compressedFile: String, fis: InputStream) {
val buffer = ByteArray(1024)
val p = GzipParameters()
p.operatingSystem = 3
FileOutputStream(compressedFile).use { fos ->
GzipCompressorOutputStream(fos, p).use { gos ->
var bytesRead: Int
while (true) {
bytesRead = fis.read(buffer)
if (bytesRead <= 0) break
gos.write(buffer, 0, bytesRead)
}
log.info("compress(gz) done: $compressedFile")
}
}
}
fun extractFile(fileName: String, outImgName: String, offset: Long, length: Int) {
if (0 == length) {
return
}
RandomAccessFile(fileName, "r").use { inRaf ->
RandomAccessFile(outImgName, "rw").use { outRaf ->
inRaf.seek(offset)
val data = ByteArray(length)
assertTrue(length == inRaf.read(data))
outRaf.write(data)
}
}
}
fun round_to_multiple(size: Long, page: Int): Long {
val remainder = size % page
return if (remainder == 0L) {
size
} else {
size + page - remainder
}
}
/*
read RSA private key
assert exp == 65537
num_bits = log2(modulus)
@return: AvbRSAPublicKeyHeader formatted bytearray
https://android.googlesource.com/platform/external/avb/+/master/libavb/avb_crypto.h#158
*/
fun encodeRSAkey(key: ByteArray): ByteArray {
val p2 = PemReader(InputStreamReader(ByteArrayInputStream(key))).readPemObject()
Assert.assertEquals("RSA PRIVATE KEY", p2.type)
val rsa = RSAPrivateKey.getInstance(p2.content)
Assert.assertEquals(65537.toBigInteger(), rsa.publicExponent)
val numBits: Int = BigIntegerMath.log2(rsa.modulus, RoundingMode.CEILING)
log.debug("modulus: " + rsa.modulus)
log.debug("numBits: $numBits")
val b = BigInteger.valueOf(2).pow(32)
val n0inv = (b - rsa.modulus.modInverse(b)).toLong()
log.debug("n0inv = $n0inv")
val r = BigInteger.valueOf(2).pow(numBits)
val rrModn = (r * r).mod(rsa.modulus)
log.debug("BB: " + numBits / 8 + ", mod_len: " + rsa.modulus.toByteArray().size + ", rrmodn = " + rrModn.toByteArray().size)
val unsignedModulo = rsa.modulus.toByteArray().sliceArray(1..numBits / 8) //remove sign byte
log.debug("unsigned modulo: " + Hex.encodeHexString(unsignedModulo))
val ret = Struct("!II${numBits / 8}b${numBits / 8}b").pack(
numBits,
n0inv,
unsignedModulo,
rrModn.toByteArray())
log.debug("rrmodn: " + Hex.encodeHexString(rrModn.toByteArray()))
log.debug("RSA: " + Hex.encodeHexString(ret))
return ret
}
//inspired by
// https://stackoverflow.com/questions/40242391/how-can-i-sign-a-raw-message-without-first-hashing-it-in-bouncy-castle
// "specifying Cipher.ENCRYPT mode or Cipher.DECRYPT mode doesn't make a difference;
// both simply perform modular exponentiation"
fun rawSign(keyPath: String, data: ByteArray): ByteArray {
val privk = Helper.readPrivateKey(keyPath)
val cipher = Cipher.getInstance("RSA/ECB/NoPadding").apply {
this.init(Cipher.ENCRYPT_MODE, privk)
this.update(data)
}
return cipher.doFinal()
}
fun rawSignOpenSsl(keyPath: String, data: ByteArray): ByteArray {
log.debug("raw input: " + Hex.encodeHexString(data))
log.debug("Raw sign data size = ${data.size}, key = $keyPath")
var ret = byteArrayOf()
val exe = DefaultExecutor()
val stdin = ByteArrayInputStream(data)
val stdout = ByteArrayOutputStream()
val stderr = ByteArrayOutputStream()
exe.streamHandler = PumpStreamHandler(stdout, stderr, stdin)
try {
exe.execute(CommandLine.parse("openssl rsautl -sign -inkey $keyPath -raw"))
ret = stdout.toByteArray()
log.debug("Raw signature size = " + ret.size)
} catch (e: ExecuteException) {
log.error("Execute error")
} finally {
log.debug("OUT: " + Hex.encodeHexString(stdout.toByteArray()))
log.debug("ERR: " + String(stderr.toByteArray()))
}
if (ret.isEmpty()) throw RuntimeException("raw sign failed")
return ret
}
fun pyAlg2java(alg: String): String {
return when (alg) {
"sha1" -> "sha-1"
"sha224" -> "sha-224"
"sha256" -> "sha-256"
"sha384" -> "sha-384"
"sha512" -> "sha-512"
else -> throw IllegalArgumentException("unknown algorithm: $alg")
}
}
@Throws(Exception::class)
fun readPrivateKey(filename: String): PrivateKey {
val spec = PKCS8EncodedKeySpec(Files.readAllBytes(Paths.get(filename)))
return KeyFactory.getInstance("RSA").generatePrivate(spec)
}
private val log = LoggerFactory.getLogger("Helper")
}
}