mirror of
https://github.com/revanced/revanced-cli.git
synced 2024-11-11 22:29:24 +01:00
feat: Added root-only adb runner (tested on emulator)
I spent almost an entire day on this, you better be happy!
This commit is contained in:
parent
c9941fe182
commit
37ecc5eaa6
@ -4,7 +4,6 @@ plugins {
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
version = "1.0"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -16,6 +15,9 @@ repositories {
|
||||
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE!
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url = uri("https://jitpack.io")
|
||||
}
|
||||
}
|
||||
|
||||
val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4"
|
||||
@ -29,8 +31,12 @@ dependencies {
|
||||
|
||||
implementation("com.google.code.gson:gson:2.9.0")
|
||||
implementation("me.tongfei:progressbar:0.9.3")
|
||||
implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead.
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
}
|
||||
|
||||
val cliMainClass = "app.revanced.cli.Main"
|
||||
|
||||
tasks {
|
||||
build {
|
||||
dependsOn(shadowJar)
|
||||
@ -42,7 +48,7 @@ tasks {
|
||||
exclude(dependency(patchesDependency))
|
||||
}
|
||||
manifest {
|
||||
attributes("Main-Class" to "app.revanced.cli.Main")
|
||||
attributes("Main-Class" to cliMainClass)
|
||||
attributes("Implementation-Title" to project.name)
|
||||
attributes("Implementation-Version" to project.version)
|
||||
}
|
||||
|
@ -1 +1,2 @@
|
||||
kotlin.code.style=official
|
||||
version = 1.0.0-dev
|
@ -1,5 +1,6 @@
|
||||
package app.revanced.cli
|
||||
|
||||
import app.revanced.cli.runner.Emulator
|
||||
import app.revanced.cli.utils.PatchLoader
|
||||
import app.revanced.cli.utils.Patches
|
||||
import app.revanced.cli.utils.Preconditions
|
||||
@ -8,6 +9,7 @@ import app.revanced.patcher.patch.PatchMetadata
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import kotlinx.cli.ArgParser
|
||||
import kotlinx.cli.ArgType
|
||||
import kotlinx.cli.default
|
||||
import kotlinx.cli.required
|
||||
import me.tongfei.progressbar.ProgressBarBuilder
|
||||
import me.tongfei.progressbar.ProgressBarStyle
|
||||
@ -23,7 +25,9 @@ class Main {
|
||||
inApk: String,
|
||||
inPatches: String,
|
||||
inIntegrations: String?,
|
||||
inOutput: String,
|
||||
inOutput: String?,
|
||||
inEmulate: String?,
|
||||
hideResults: Boolean,
|
||||
) {
|
||||
val bar = ProgressBarBuilder()
|
||||
.setTaskName("Working..")
|
||||
@ -35,12 +39,9 @@ class Main {
|
||||
.setExtraMessage("Initializing")
|
||||
val apk = Preconditions.isFile(inApk)
|
||||
val patchesFile = Preconditions.isFile(inPatches)
|
||||
val output = Preconditions.isDirectory(inOutput)
|
||||
bar.step()
|
||||
|
||||
val patcher = Patcher(
|
||||
apk,
|
||||
)
|
||||
val patcher = Patcher(apk)
|
||||
|
||||
inIntegrations?.let {
|
||||
bar.reset().maxHint(1)
|
||||
@ -53,12 +54,14 @@ class Main {
|
||||
bar.reset().maxHint(1)
|
||||
.extraMessage = "Loading patches"
|
||||
PatchLoader.injectPatches(patchesFile)
|
||||
bar.step()
|
||||
|
||||
val patches = Patches.loadPatches().map { it() }
|
||||
patcher.addPatches(patches)
|
||||
bar.step()
|
||||
|
||||
bar.reset().maxHint(1)
|
||||
.extraMessage = "Resolving signatures"
|
||||
patcher.resolveSignatures()
|
||||
bar.step()
|
||||
|
||||
val amount = patches.size.toLong()
|
||||
bar.reset().maxHint(amount)
|
||||
@ -70,24 +73,41 @@ class Main {
|
||||
bar.reset().maxHint(-1)
|
||||
.extraMessage = "Generating dex files"
|
||||
val dexFiles = patcher.save()
|
||||
bar.reset().maxHint(dexFiles.size.toLong())
|
||||
.extraMessage = "Saving dex files"
|
||||
dexFiles.forEach { (dexName, dexData) ->
|
||||
Files.write(File(output, dexName).toPath(), dexData.data)
|
||||
bar.step()
|
||||
|
||||
inOutput?.let {
|
||||
val output = Preconditions.isDirectory(it)
|
||||
val amount = dexFiles.size.toLong()
|
||||
bar.reset().maxHint(amount)
|
||||
.extraMessage = "Saving dex files"
|
||||
dexFiles.forEach { (dexName, dexData) ->
|
||||
Files.write(File(output, dexName).toPath(), dexData.data)
|
||||
bar.step()
|
||||
}
|
||||
bar.stepTo(amount)
|
||||
}
|
||||
|
||||
bar.close()
|
||||
|
||||
inEmulate?.let { device ->
|
||||
Emulator.emulate(
|
||||
apk,
|
||||
dexFiles,
|
||||
device
|
||||
)
|
||||
}
|
||||
|
||||
println("All done!")
|
||||
printResults(results)
|
||||
if (!hideResults) {
|
||||
printResults(results)
|
||||
}
|
||||
}
|
||||
|
||||
private fun printResults(results: Map<PatchMetadata, Result<PatchResult>>) {
|
||||
for ((metadata, result) in results) {
|
||||
if (result.isSuccess) {
|
||||
println("${metadata.name} was applied successfully!")
|
||||
println("${metadata.shortName} was applied successfully!")
|
||||
} else {
|
||||
println("${metadata.name} failed to apply! Cause:")
|
||||
println("${metadata.shortName} failed to apply! Cause:")
|
||||
result.exceptionOrNull()!!.printStackTrace()
|
||||
}
|
||||
}
|
||||
@ -98,6 +118,9 @@ class Main {
|
||||
println("$CLI_NAME version $CLI_VERSION")
|
||||
val parser = ArgParser(CLI_NAME)
|
||||
|
||||
// TODO: add some kind of incremental building, so merging integrations can be skipped.
|
||||
// this can be achieved manually, but doing it automatically is better.
|
||||
|
||||
val apk by parser.option(
|
||||
ArgType.String,
|
||||
fullName = "apk",
|
||||
@ -121,7 +144,18 @@ class Main {
|
||||
fullName = "output",
|
||||
shortName = "o",
|
||||
description = "Output directory"
|
||||
).required()
|
||||
)
|
||||
val emulate by parser.option(
|
||||
ArgType.String,
|
||||
fullName = "run-on",
|
||||
description = "After the CLI is done building, which ADB device should it run on?"
|
||||
)
|
||||
// TODO: package name
|
||||
val hideResults by parser.option(
|
||||
ArgType.Boolean,
|
||||
fullName = "hide-results",
|
||||
description = "Don't print the patch results."
|
||||
).default(false)
|
||||
|
||||
parser.parse(args)
|
||||
runCLI(
|
||||
@ -129,6 +163,8 @@ class Main {
|
||||
patches,
|
||||
integrations,
|
||||
output,
|
||||
emulate,
|
||||
hideResults,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
140
src/main/kotlin/app/revanced/cli/runner/AdbRunner.kt
Normal file
140
src/main/kotlin/app/revanced/cli/runner/AdbRunner.kt
Normal file
@ -0,0 +1,140 @@
|
||||
package app.revanced.cli.runner
|
||||
|
||||
import app.revanced.cli.utils.DexReplacer
|
||||
import app.revanced.cli.utils.Scripts
|
||||
import app.revanced.cli.utils.signer.Signer
|
||||
import me.tongfei.progressbar.ProgressBar
|
||||
import me.tongfei.progressbar.ProgressBarBuilder
|
||||
import me.tongfei.progressbar.ProgressBarStyle
|
||||
import org.jf.dexlib2.writer.io.MemoryDataStore
|
||||
import se.vidstige.jadb.JadbConnection
|
||||
import se.vidstige.jadb.JadbDevice
|
||||
import se.vidstige.jadb.RemoteFile
|
||||
import se.vidstige.jadb.ShellProcessBuilder
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
object Emulator {
|
||||
fun emulate(
|
||||
apk: File,
|
||||
dexFiles: Map<String, MemoryDataStore>,
|
||||
deviceName: String
|
||||
) {
|
||||
lateinit var dvc: JadbDevice
|
||||
pbar("Initializing").use { bar ->
|
||||
dvc = JadbConnection().findDevice(deviceName)
|
||||
?: throw IllegalArgumentException("No such device with name $deviceName")
|
||||
if (!dvc.hasSu())
|
||||
throw IllegalArgumentException("Device $deviceName is not rooted or does not have su")
|
||||
bar.step()
|
||||
}
|
||||
|
||||
lateinit var tmpFile: File // we need this file at the end to clean up.
|
||||
pbar("Generating APK file", 3).use { bar ->
|
||||
bar.step().extraMessage = "Creating APK file"
|
||||
tmpFile = Files.createTempFile("rvc-cli", ".apk").toFile()
|
||||
apk.copyTo(tmpFile, true)
|
||||
|
||||
bar.step().extraMessage = "Replacing dex files"
|
||||
DexReplacer.replaceDex(tmpFile, dexFiles)
|
||||
|
||||
bar.step().extraMessage = "Signing APK file"
|
||||
Signer.signApk(tmpFile)
|
||||
}
|
||||
|
||||
pbar("Running application", 6, false).use { bar ->
|
||||
bar.step().extraMessage = "Pushing mount scripts"
|
||||
dvc.push(Scripts.MOUNT_SCRIPT, RemoteFile(Scripts.SCRIPT_PATH))
|
||||
dvc.cmd(Scripts.CREATE_DIR_COMMAND).assertZero()
|
||||
dvc.cmd(Scripts.MV_MOUNT_COMMAND).assertZero()
|
||||
dvc.cmd(Scripts.CHMOD_MOUNT_COMMAND).assertZero()
|
||||
|
||||
bar.step().extraMessage = "Pushing APK file"
|
||||
dvc.push(tmpFile, RemoteFile(Scripts.APK_PATH))
|
||||
|
||||
bar.step().extraMessage = "Mounting APK file"
|
||||
dvc.cmd(Scripts.STOP_APP_COMMAND).startAndWait()
|
||||
dvc.cmd(Scripts.START_MOUNT_COMMAND).assertZero()
|
||||
|
||||
bar.step().extraMessage = "Starting APK file"
|
||||
dvc.cmd(Scripts.START_APP_COMMAND).assertZero()
|
||||
|
||||
bar.step().setExtraMessage("Debugging APK file").refresh()
|
||||
println("\nWaiting until app is closed.")
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val p = dvc.cmd(Scripts.LOGCAT_COMMAND)
|
||||
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
|
||||
.redirectError(ProcessBuilder.Redirect.INHERIT)
|
||||
.useExecutor(executor)
|
||||
.start()
|
||||
Thread.sleep(250) // give the app some time to start up.
|
||||
while (dvc.cmd(Scripts.PIDOF_APP_COMMAND).startAndWait() == 0) {
|
||||
Thread.sleep(250)
|
||||
}
|
||||
println("App closed, continuing.")
|
||||
p.destroy()
|
||||
executor.shutdown()
|
||||
|
||||
bar.step().extraMessage = "Unmounting APK file"
|
||||
var exitCode: Int
|
||||
do {
|
||||
exitCode = dvc.cmd(Scripts.UNMOUNT_COMMAND).startAndWait()
|
||||
} while (exitCode != 0)
|
||||
}
|
||||
|
||||
tmpFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun JadbDevice.push(s: String, remoteFile: RemoteFile) =
|
||||
this.push(s.byteInputStream(), System.currentTimeMillis(), 644, remoteFile)
|
||||
|
||||
private fun JadbConnection.findDevice(device: String): JadbDevice? {
|
||||
return devices.find { it.serial == device }
|
||||
}
|
||||
|
||||
private fun JadbDevice.cmd(s: String): ShellProcessBuilder {
|
||||
val args = s.split(" ") as ArrayList<String>
|
||||
val cmd = args.removeFirst()
|
||||
return shellProcessBuilder(cmd, *args.toTypedArray())
|
||||
}
|
||||
|
||||
private fun JadbDevice.hasSu(): Boolean {
|
||||
return cmd("su -h").startAndWait() == 0
|
||||
}
|
||||
|
||||
private fun ShellProcessBuilder.startAndWait(): Int {
|
||||
return start().waitFor()
|
||||
}
|
||||
|
||||
private fun ShellProcessBuilder.assertZero() {
|
||||
if (startAndWait() != 0) {
|
||||
val cmd = getcmd()
|
||||
throw IllegalStateException("ADB returned non-zero status code for command: $cmd")
|
||||
}
|
||||
}
|
||||
|
||||
private fun pbar(task: String, steps: Long = 1, update: Boolean = true): ProgressBar {
|
||||
val b = ProgressBarBuilder().setTaskName(task)
|
||||
if (update) b
|
||||
.setUpdateIntervalMillis(250)
|
||||
.continuousUpdate()
|
||||
return b
|
||||
.setStyle(ProgressBarStyle.ASCII)
|
||||
.build()
|
||||
.maxHint(steps + 1)
|
||||
}
|
||||
|
||||
private fun ProgressBar.use(block: (ProgressBar) -> Unit) {
|
||||
block(this)
|
||||
stepTo(max) // step to 100%
|
||||
extraMessage = "" // clear extra message
|
||||
close()
|
||||
}
|
||||
|
||||
private fun ShellProcessBuilder.getcmd(): String {
|
||||
val f = this::class.java.getDeclaredField("command")
|
||||
f.isAccessible = true
|
||||
return f.get(this) as String
|
||||
}
|
31
src/main/kotlin/app/revanced/cli/utils/DexReplacer.kt
Normal file
31
src/main/kotlin/app/revanced/cli/utils/DexReplacer.kt
Normal file
@ -0,0 +1,31 @@
|
||||
package app.revanced.cli.utils
|
||||
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import org.jf.dexlib2.writer.io.MemoryDataStore
|
||||
import java.io.File
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
|
||||
val NAMER = BasicDexFileNamer()
|
||||
|
||||
object DexReplacer {
|
||||
fun replaceDex(source: File, dexFiles: Map<String, MemoryDataStore>) {
|
||||
FileSystems.newFileSystem(
|
||||
source.toPath(),
|
||||
null
|
||||
).use { fs ->
|
||||
// Delete all classes?.dex files
|
||||
Files.walk(fs.rootDirectories.first()).forEach {
|
||||
if (
|
||||
it.toString().endsWith(".dex") &&
|
||||
NAMER.isValidName(it.fileName.toString())
|
||||
) Files.delete(it)
|
||||
}
|
||||
// Write new dex files
|
||||
dexFiles
|
||||
.forEach { (dexName, dexData) ->
|
||||
Files.write(fs.getPath("/$dexName"), dexData.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
src/main/kotlin/app/revanced/cli/utils/Scripts.kt
Normal file
34
src/main/kotlin/app/revanced/cli/utils/Scripts.kt
Normal file
@ -0,0 +1,34 @@
|
||||
package app.revanced.cli.utils
|
||||
|
||||
// TODO: make this a class with PACKAGE_NAME as argument, then use that everywhere.
|
||||
// make sure
|
||||
object Scripts {
|
||||
private const val PACKAGE_NAME = "com.google.android.youtube"
|
||||
private const val DATA_PATH = "/data/adb/ReVanced"
|
||||
const val APK_PATH = "/sdcard/base.apk"
|
||||
const val SCRIPT_PATH = "/sdcard/mount.sh"
|
||||
|
||||
val MOUNT_SCRIPT =
|
||||
"""
|
||||
base_path="$DATA_PATH/base.apk"
|
||||
stock_path=${'$'}{ pm path $PACKAGE_NAME | grep base | sed 's/package://g' }
|
||||
umount -l ${'$'}stock_path
|
||||
rm ${'$'}base_path
|
||||
mv "$APK_PATH" ${'$'}base_path
|
||||
chmod 644 ${'$'}base_path
|
||||
chown system:system ${'$'}base_path
|
||||
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
|
||||
mount -o bind ${'$'}base_path ${'$'}stock_path
|
||||
""".trimIndent()
|
||||
|
||||
const val PIDOF_APP_COMMAND = "pidof -s $PACKAGE_NAME"
|
||||
private const val PIDOF_APP = "\$($PIDOF_APP_COMMAND)"
|
||||
const val CREATE_DIR_COMMAND = "su -c \"mkdir -p $DATA_PATH/\""
|
||||
const val MV_MOUNT_COMMAND = "su -c \"mv /sdcard/mount.sh $DATA_PATH/\""
|
||||
const val CHMOD_MOUNT_COMMAND = "su -c \"chmod +x $DATA_PATH/mount.sh\""
|
||||
const val START_MOUNT_COMMAND = "su -c $DATA_PATH/mount.sh"
|
||||
const val UNMOUNT_COMMAND = "su -c \"umount -l $(pm path $PACKAGE_NAME | grep base | sed 's/package://g')\""
|
||||
const val LOGCAT_COMMAND = "su -c \"logcat -c && logcat --pid=$PIDOF_APP\""
|
||||
const val STOP_APP_COMMAND = "su -c \"kill $PIDOF_APP\""
|
||||
const val START_APP_COMMAND = "monkey -p $PACKAGE_NAME 1"
|
||||
}
|
9
src/main/kotlin/app/revanced/cli/utils/signer/KeySet.kt
Normal file
9
src/main/kotlin/app/revanced/cli/utils/signer/KeySet.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package app.revanced.cli.utils.signer
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
data class KeySet(
|
||||
val publicKey: X509Certificate,
|
||||
val privateKey: PrivateKey
|
||||
)
|
211
src/main/kotlin/app/revanced/cli/utils/signer/Signer.kt
Normal file
211
src/main/kotlin/app/revanced/cli/utils/signer/Signer.kt
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright (c) 2021 Juby210 & Vendicated
|
||||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
package app.revanced.cli.utils.signer
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.jcajce.JcaCertStore
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cms.*
|
||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import org.bouncycastle.operator.DigestCalculatorProvider
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
|
||||
import org.bouncycastle.util.encoders.Base64
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.*
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.Manifest
|
||||
import java.util.regex.Pattern
|
||||
|
||||
|
||||
const val CN = "ReVanced"
|
||||
val PASSWORD = "revanced".toCharArray() // TODO: make it secure; random password should be enough
|
||||
|
||||
/**
|
||||
* APK Signer.
|
||||
* @author Aliucord authors
|
||||
* @author ReVanced Team
|
||||
*/
|
||||
object Signer {
|
||||
private fun newKeystore(out: File) {
|
||||
val key = createKey()
|
||||
val privateKS = KeyStore.getInstance("BKS", "BC")
|
||||
privateKS.load(null, PASSWORD)
|
||||
privateKS.setKeyEntry("alias", key.privateKey, PASSWORD, arrayOf(key.publicKey))
|
||||
privateKS.store(FileOutputStream(out), PASSWORD)
|
||||
}
|
||||
|
||||
private fun createKey(): KeySet {
|
||||
val gen = KeyPairGenerator.getInstance("RSA")
|
||||
gen.initialize(2048)
|
||||
val pair = gen.generateKeyPair()
|
||||
var serialNumber: BigInteger
|
||||
do serialNumber =
|
||||
BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
|
||||
val x500Name = X500Name("CN=$CN")
|
||||
val builder = X509v3CertificateBuilder(
|
||||
x500Name,
|
||||
serialNumber,
|
||||
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
|
||||
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
|
||||
Locale.ENGLISH,
|
||||
x500Name,
|
||||
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
|
||||
)
|
||||
val signer: ContentSigner = JcaContentSignerBuilder("SHA1withRSA").build(pair.private)
|
||||
return KeySet(JcaX509CertificateConverter().getCertificate(builder.build(signer)), pair.private)
|
||||
}
|
||||
|
||||
private val stripPattern: Pattern = Pattern.compile("^META-INF/(.*)[.](MF|SF|RSA|DSA)$")
|
||||
|
||||
// based on https://gist.github.com/mmuszkow/10288441
|
||||
// and https://github.com/fornwall/apksigner/blob/master/src/main/java/net/fornwall/apksigner/ZipSigner.java
|
||||
fun signApk(apkFile: File) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
val ks = File(apkFile.parent, "revanced-cli.keystore")
|
||||
if (!ks.exists()) newKeystore(ks)
|
||||
|
||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
val keySet = KeySet(
|
||||
(keyStore.getCertificate(alias) as X509Certificate),
|
||||
(keyStore.getKey(alias, PASSWORD) as PrivateKey)
|
||||
)
|
||||
|
||||
val zip = FileSystems.newFileSystem(apkFile.toPath(), null)
|
||||
|
||||
val dig = MessageDigest.getInstance("SHA1")
|
||||
val digests: MutableMap<String, String> = LinkedHashMap()
|
||||
|
||||
for (entry in zip.allEntries) {
|
||||
val name = entry.toString()
|
||||
if (stripPattern.matcher(name).matches()) {
|
||||
Files.delete(entry)
|
||||
} else {
|
||||
digests[name] = toBase64(dig.digest(Files.readAllBytes(entry)))
|
||||
}
|
||||
}
|
||||
|
||||
val sectionDigests: MutableMap<String, String> = LinkedHashMap()
|
||||
var manifest = Manifest()
|
||||
var attrs = manifest.mainAttributes
|
||||
attrs[Attributes.Name.MANIFEST_VERSION] = "1.0"
|
||||
attrs[Attributes.Name("Created-By")] = CN
|
||||
|
||||
val digestAttr = Attributes.Name("SHA1-Digest")
|
||||
for ((name, value) in digests) {
|
||||
val attributes = Attributes()
|
||||
attributes[digestAttr] = value
|
||||
manifest.entries[name] = attributes
|
||||
sectionDigests[name] = hashEntrySection(name, attributes, dig)
|
||||
}
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
zip.writeFile(JarFile.MANIFEST_NAME, baos.toByteArray())
|
||||
}
|
||||
|
||||
val manifestHash = getManifestHash(manifest, dig)
|
||||
val tmpManifest = Manifest()
|
||||
tmpManifest.mainAttributes.putAll(attrs)
|
||||
val manifestMainHash = getManifestHash(tmpManifest, dig)
|
||||
|
||||
manifest = Manifest()
|
||||
attrs = manifest.mainAttributes
|
||||
attrs[Attributes.Name.SIGNATURE_VERSION] = "1.0"
|
||||
attrs[Attributes.Name("Created-By")] = CN
|
||||
attrs[Attributes.Name("SHA1-Digest-Manifest")] = manifestHash
|
||||
attrs[Attributes.Name("SHA1-Digest-Manifest-Main-Attributes")] = manifestMainHash
|
||||
|
||||
for ((key, value) in sectionDigests) {
|
||||
val attributes = Attributes()
|
||||
attributes[digestAttr] = value
|
||||
manifest.entries[key] = attributes
|
||||
}
|
||||
var sigBytes: ByteArray
|
||||
ByteArrayOutputStream().use { sigStream ->
|
||||
manifest.write(sigStream)
|
||||
sigBytes = sigStream.toByteArray()
|
||||
zip.writeFile("META-INF/CERT.SF", sigBytes)
|
||||
}
|
||||
|
||||
val signature = signSigFile(keySet, sigBytes)
|
||||
zip.writeFile("META-INF/CERT.RSA", signature)
|
||||
|
||||
zip.close()
|
||||
}
|
||||
|
||||
private fun hashEntrySection(name: String, attrs: Attributes, dig: MessageDigest): String {
|
||||
val manifest = Manifest()
|
||||
manifest.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
val emptyLen = baos.toByteArray().size
|
||||
manifest.entries[name] = attrs
|
||||
baos.reset()
|
||||
manifest.write(baos)
|
||||
var ob = baos.toByteArray()
|
||||
ob = Arrays.copyOfRange(ob, emptyLen, ob.size)
|
||||
return toBase64(dig.digest(ob))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getManifestHash(manifest: Manifest, dig: MessageDigest): String {
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
return toBase64(dig.digest(baos.toByteArray()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun signSigFile(keySet: KeySet, content: ByteArray): ByteArray {
|
||||
val msg: CMSTypedData = CMSProcessableByteArray(content)
|
||||
val certs = JcaCertStore(Collections.singletonList(keySet.publicKey))
|
||||
val gen = CMSSignedDataGenerator()
|
||||
val jcaContentSignerBuilder = JcaContentSignerBuilder("SHA1withRSA")
|
||||
val sha1Signer: ContentSigner = jcaContentSignerBuilder.build(keySet.privateKey)
|
||||
val jcaDigestCalculatorProviderBuilder = JcaDigestCalculatorProviderBuilder()
|
||||
val digestCalculatorProvider: DigestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build()
|
||||
val jcaSignerInfoGeneratorBuilder = JcaSignerInfoGeneratorBuilder(digestCalculatorProvider)
|
||||
jcaSignerInfoGeneratorBuilder.setDirectSignature(true)
|
||||
val signerInfoGenerator: SignerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.publicKey)
|
||||
gen.addSignerInfoGenerator(signerInfoGenerator)
|
||||
gen.addCertificates(certs)
|
||||
val sigData: CMSSignedData = gen.generate(msg, false)
|
||||
return sigData.toASN1Structure().getEncoded("DER")
|
||||
}
|
||||
|
||||
private fun toBase64(data: ByteArray): String {
|
||||
return String(Base64.encode(data))
|
||||
}
|
||||
}
|
||||
|
||||
private val java.nio.file.FileSystem.allEntries: List<Path>
|
||||
get() = buildList {
|
||||
this@allEntries.rootDirectories.forEach { dir ->
|
||||
Files.walk(dir).filter(Files::isRegularFile).forEach { file ->
|
||||
this@buildList.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun java.nio.file.FileSystem.writeFile(path: String, bytes: ByteArray) {
|
||||
Files.write(this.getPath("/$path"), bytes)
|
||||
}
|
Loading…
Reference in New Issue
Block a user