diff --git a/docs/0_prerequisites.md b/docs/0_prerequisites.md index d8654d84..ed5660f7 100644 --- a/docs/0_prerequisites.md +++ b/docs/0_prerequisites.md @@ -5,7 +5,7 @@ To use ReVanced CLI, you will need to fulfil specific requirements. ## 🤝 Requirements - Java SDK 11 (Azul Zulu JDK or OpenJDK) -- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to deploy the patched APK file on your device +- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to install the patched APK file on your device - An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7) - ReVanced Patches - ReVanced Integrations, if the patches require it diff --git a/docs/1_usage.md b/docs/1_usage.md index 932fdb21..a03abaa8 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -10,7 +10,7 @@ Learn how to ReVanced CLI. adb shell exit ``` - If you want to deploy the patched APK file on your device by mounting it on top of the original APK file, you will need root access. This is optional. + If you want to install the patched APK file on your device by mounting it on top of the original APK file, you will need root access. This is optional. ```bash adb shell su -c exit @@ -33,8 +33,7 @@ Learn how to ReVanced CLI. - ### 📃 List patches from supplied patch bundles ```bash - java -jar revanced-cli.jar \ - list-patches \ + java -jar revanced-cli.jar list-patches \ --with-packages \ --with-versions \ --with-options \ @@ -49,31 +48,31 @@ Learn how to ReVanced CLI. > **Note**: The `options.json` file will be generated at the first time you use ReVanced CLI to patch an APK file for now. This will be changed in the future. -- ### 💉 Use ReVanced CLI to patch an APK file but deploy without root permissions +- ### 💉 Use ReVanced CLI to patch an APK file but install without root permissions - This will deploy the patched APK file on your device by installing it. + This will install the patched APK file regularly on your device. ```bash - java -jar revanced-cli.jar \ - -a input.apk \ - -o patched-output.apk \ + java -jar revanced-cli.jar patch \ -b revanced-patches.jar \ - -d device-serial + -o patched-output.apk \ + -d device-serial \ + input-apk ``` -- ### 👾 Use ReVanced CLI to patch an APK file but deploy with root permissions +- ### 👾 Use ReVanced CLI to patch an APK file but install with root permissions - This will deploy the patched APK file on your device by mounting it on top of the original APK file. + This will install the patched APK file on your device by mounting it on top of the original APK file. ```bash adb install input.apk - java -jar revanced-cli.jar \ - -a input.apk \ + java -jar revanced-cli.jar patch \ -o patched-output.apk \ -b revanced-patches.jar \ - -e vanced-microg-support \ + -e some-patch \ -d device-serial \ - --mount + --mount \ + input-apk ``` > **Note**: Some patches from [ReVanced Patches](https://github.com/revanced/revanced-patches) also require [ReVanced Integrations](https://github.com/revanced/revanced-integrations). Supply them with the option `-m`. ReVanced Patcher will merge ReVanced Integrations automatically, depending on if the supplied patches require them. @@ -81,8 +80,7 @@ Learn how to ReVanced CLI. - ### 🗑️ Uninstall a patched ```bash - java -jar revanced-cli.jar \ - uninstall \ + java -jar revanced-cli.jar uninstall \ -p package-name \ device-serial ``` diff --git a/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt b/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt deleted file mode 100644 index d67c2ade..00000000 --- a/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt +++ /dev/null @@ -1,37 +0,0 @@ -package app.revanced.cli.aligning - -import app.revanced.cli.command.MainCommand.logger -import app.revanced.patcher.PatcherResult -import app.revanced.utils.signing.align.ZipAligner -import app.revanced.utils.signing.align.zip.ZipFile -import app.revanced.utils.signing.align.zip.structures.ZipEntry -import java.io.File - -object Aligning { - fun align(result: PatcherResult, inputFile: File, outputFile: File) { - logger.info("Aligning ${inputFile.name} to ${outputFile.name}") - - if (outputFile.exists()) outputFile.delete() - - ZipFile(outputFile).use { file -> - result.dexFiles.forEach { - file.addEntryCompressData( - ZipEntry.createWithName(it.name), - it.stream.readBytes() - ) - } - - result.resourceFile?.let { - file.copyEntriesFromFileAligned( - ZipFile(it), - ZipAligner::getEntryAlignment - ) - } - - file.copyEntriesFromFileAligned( - ZipFile(inputFile), - ZipAligner::getEntryAlignment - ) - } - } -} diff --git a/src/main/kotlin/app/revanced/cli/command/Main.kt b/src/main/kotlin/app/revanced/cli/command/Main.kt new file mode 100644 index 00000000..d33e412b --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/Main.kt @@ -0,0 +1,39 @@ +package app.revanced.cli.command + +import app.revanced.cli.logging.impl.DefaultCliLogger +import app.revanced.patcher.patch.PatchClass +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.IVersionProvider +import java.util.* + +fun main(args: Array) { + CommandLine(Main).execute(*args) +} + +internal typealias PatchList = List + +internal val logger = DefaultCliLogger() + +object CLIVersionProvider : IVersionProvider { + override fun getVersion(): Array { + Properties().apply { + load(Main::class.java.getResourceAsStream("/app/revanced/cli/version.properties")) + }.let { + return arrayOf("ReVanced CLI v${it.getProperty("version")}") + } + } +} + +@Command( + name = "revanced-cli", + description = ["Command line application to use ReVanced"], + mixinStandardHelpOptions = true, + versionProvider = CLIVersionProvider::class, + subcommands = [ + ListPatchesCommand::class, + PatchCommand::class, + UninstallCommand::class + ] +) +internal object Main \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt new file mode 100644 index 00000000..279fce81 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -0,0 +1,412 @@ +package app.revanced.cli.command + +import app.revanced.cli.patcher.logging.impl.PatcherLogger +import app.revanced.patcher.PatchBundleLoader +import app.revanced.patcher.Patcher +import app.revanced.patcher.PatcherOptions +import app.revanced.patcher.PatcherResult +import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages +import app.revanced.patcher.extensions.PatchExtensions.include +import app.revanced.patcher.extensions.PatchExtensions.patchName +import app.revanced.utils.Options +import app.revanced.utils.Options.setOptions +import app.revanced.utils.adb.AdbManager +import app.revanced.utils.align.ZipAligner +import app.revanced.utils.align.zip.ZipFile +import app.revanced.utils.align.zip.structures.ZipEntry +import app.revanced.utils.signing.ApkSigner +import app.revanced.utils.signing.SigningOptions +import kotlinx.coroutines.runBlocking +import picocli.CommandLine +import picocli.CommandLine.Help.Visibility.ALWAYS +import java.io.File + + +@CommandLine.Command( + name = "patch", + description = ["Patch the supplied APK file with the supplied patches and integrations"] +) +internal object PatchCommand: Runnable { + @CommandLine.Parameters( + description = ["APK file to be patched"], + arity = "1..1" + ) + lateinit var apk: File + + @CommandLine.Option( + names = ["-b", "--bundle"], + description = ["One or more bundles of patches"], + required = true + ) + var patchBundles = emptyList() + + @CommandLine.Option( + names = ["-m", "--merge"], + description = ["One or more DEX files or containers to merge into the APK"] + ) + var integrations = listOf() + + @CommandLine.Option( + names = ["-i", "--include"], + description = ["List of patches to include"] + ) + var includedPatches = arrayOf() + + @CommandLine.Option( + names = ["-e", "--exclude"], + description = ["List of patches to exclude"] + ) + var excludedPatches = arrayOf() + + @CommandLine.Option( + names = ["--options"], + description = ["Path to patch options JSON file"], + showDefaultValue = ALWAYS + ) + var optionsFile: File = File("options.json") + + @CommandLine.Option( + names = ["--exclusive"], + description = ["Only include patches that are explicitly specified to be included"], + showDefaultValue = ALWAYS + ) + var exclusive = false + + @CommandLine.Option( + names = ["--experimental"], + description = ["Ignore patches incompatibility to versions"], + showDefaultValue = ALWAYS + ) + var experimental: Boolean = false + + @CommandLine.Option( + names = ["-o", "--out"], + description = ["Path to save the patched APK file to"], + required = true + ) + lateinit var outputFilePath: File + + @CommandLine.Option( + names = ["-d", "--device-serial"], + description = ["ADB device serial to install to"], + showDefaultValue = ALWAYS + ) + var deviceSerial: String? = null + + @CommandLine.Option( + names = ["--mount"], + description = ["Install by mounting the patched package"], + showDefaultValue = ALWAYS + ) + var mount: Boolean = false + + @CommandLine.Option( + names = ["--common-name"], + description = ["The common name of the signer of the patched APK file"], + showDefaultValue = ALWAYS + + ) + var commonName = "ReVanced" + + @CommandLine.Option( + names = ["--keystore"], + description = ["Path to the keystore to sign the patched APK file with"] + ) + var keystorePath: String? = null + + @CommandLine.Option( + names = ["--password"], + description = ["The password of the keystore to sign the patched APK file with"] + ) + var password = "ReVanced" + + @CommandLine.Option( + names = ["-r", "--resource-cache"], + description = ["Path to temporary resource cache directory"], + showDefaultValue = ALWAYS + ) + var resourceCachePath = File("revanced-resource-cache") + + @CommandLine.Option( + names = ["--custom-aapt2-binary"], + description = ["Path to a custom AAPT binary to compile resources with"] + ) + var aaptBinaryPath = File("") + + @CommandLine.Option( + names = ["-p", "--purge"], + description = ["Purge the temporary resource cache directory after patching"], + showDefaultValue = ALWAYS + ) + var purge: Boolean = false + + override fun run() { + // region Prepare + + if (!apk.exists()) { + logger.error("Input file ${apk.name} does not exist") + return + } + + val adbManager = deviceSerial?.let { serial -> + if (mount) AdbManager.RootAdbManager(serial, logger) else AdbManager.UserAdbManager( + serial, + logger + ) + } + + // endregion + + // region Load patches + + logger.info("Loading patches") + + val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray()) + val integrations = integrations + + logger.info("Setting up patch options") + + optionsFile.let { + if (it.exists()) patches.setOptions(it, logger) + else Options.serialize(patches, prettyPrint = true).let(it::writeText) + } + + // endregion + + // region Patch + + val patcher = Patcher( + PatcherOptions( + apk, + resourceCachePath, + aaptBinaryPath.absolutePath, + resourceCachePath.absolutePath, + PatcherLogger + ) + ) + + val result = patcher.apply { + acceptIntegrations(integrations) + acceptPatches(filterPatchSelection(patches)) + + // Execute patches. + runBlocking { + apply(false).collect { patchResult -> + patchResult.exception?.let { + logger.error("${patchResult.patchName} failed:\n${patchResult.exception}") + } ?: logger.info("${patchResult.patchName} succeeded") + } + } + }.get() + + patcher.close() + + // endregion + + // region Finish + + val alignAndSignedFile = sign( + apk.newAlignedFile( + result, + resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk") + ) + ) + + logger.info("Copying to ${outputFilePath.name}") + alignAndSignedFile.copyTo(outputFilePath, overwrite = true) + + adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName)) + + if (purge) { + logger.info("Purging temporary files") + outputFilePath.delete() + purge(resourceCachePath) + } + + // endregion + } + + + /** + * Filter the patches to be added to the patcher. The filter is based on the following: + * - [includedPatches] (explicitly included) + * - [excludedPatches] (explicitly excluded) + * - [exclusive] (only include patches that are explicitly included) + * - [experimental] (ignore patches incompatibility to versions) + * - package name and version of the input APK file (if [experimental] is false) + * + * @param patches The patches to filter. + * @return The filtered patches. + */ + private fun Patcher.filterPatchSelection(patches: PatchList) = buildList { + val packageName = context.packageMetadata.packageName + val packageVersion = context.packageMetadata.packageVersion + + patches.forEach patch@{ patch -> + val formattedPatchName = patch.patchName.lowercase().replace(" ", "-") + + /** + * Check if the patch is explicitly excluded. + * + * Cases: + * 1. -e patch.name + * 2. -i patch.name -e patch.name + */ + + /** + * Check if the patch is explicitly excluded. + * + * Cases: + * 1. -e patch.name + * 2. -i patch.name -e patch.name + */ + + val excluded = excludedPatches.contains(formattedPatchName) + if (excluded) return@patch logger.info("Excluding ${patch.patchName}") + + /** + * Check if the patch is constrained to packages. + */ + + /** + * Check if the patch is constrained to packages. + */ + + patch.compatiblePackages?.let { packages -> + packages.singleOrNull { it.name == packageName }?.let { `package` -> + /** + * Check if the package version matches. + * If experimental is true, version matching will be skipped. + */ + + /** + * Check if the package version matches. + * If experimental is true, version matching will be skipped. + */ + + val matchesVersion = experimental || `package`.versions.let { + it.isEmpty() || it.any { version -> version == packageVersion } + } + + if (!matchesVersion) return@patch logger.warn( + "${patch.patchName} is incompatible with version $packageVersion. " + + "This patch is only compatible with version " + + packages.joinToString(";") { `package` -> + "${`package`.name}: ${`package`.versions.joinToString(", ")}" + } + ) + + } ?: return@patch logger.trace( + "${patch.patchName} is incompatible with $packageName. " + + "This patch is only compatible with " + + packages.joinToString(", ") { `package` -> `package`.name } + ) + + return@let + } ?: logger.trace("$formattedPatchName: No constraint on packages.") + + /** + * Check if the patch is explicitly included. + * + * Cases: + * 1. --exclusive + * 2. --exclusive -i patch.name + */ + + /** + * Check if the patch is explicitly included. + * + * Cases: + * 1. --exclusive + * 2. --exclusive -i patch.name + */ + + val explicitlyIncluded = includedPatches.contains(formattedPatchName) + + val implicitlyIncluded = !exclusive && patch.include // Case 3. + val exclusivelyIncluded = exclusive && explicitlyIncluded // Case 2. + + val included = implicitlyIncluded || exclusivelyIncluded + if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1. + + logger.trace("Adding $formattedPatchName") + + add(patch) + } + } + + /** + * Create a new aligned APK file. + * + * @param result The result of the patching process. + * @param outputFile The file to save the aligned APK to. + */ + private fun File.newAlignedFile( + result: PatcherResult, + outputFile: File + ): File { + logger.info("Aligning $name to ${outputFile.name}") + + if (outputFile.exists()) outputFile.delete() + + ZipFile(outputFile).use { file -> + result.dexFiles.forEach { + file.addEntryCompressData( + ZipEntry.createWithName(it.name), + it.stream.readBytes() + ) + } + + result.resourceFile?.let { + file.copyEntriesFromFileAligned( + ZipFile(it), + ZipAligner::getEntryAlignment + ) + } + + // TODO: Do not compress result.doNotCompress + + file.copyEntriesFromFileAligned( + ZipFile(this), + ZipAligner::getEntryAlignment + ) + } + + return outputFile + } + + /** + * Sign the APK file. + * + * @param inputFile The APK file to sign. + * @return The signed APK file. If [mount] is true, the input file will be returned. + */ + private fun sign(inputFile: File) = if (mount) + inputFile + else { + logger.info("Signing ${inputFile.name}") + + val keyStoreFilePath = keystorePath ?: outputFilePath + .absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath + + val options = SigningOptions( + commonName, + password, + keyStoreFilePath + ) + + ApkSigner(options) + .signApk( + inputFile, + resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk") + ) + } + + private fun purge(resourceCachePath: File) { + val result = if (resourceCachePath.deleteRecursively()) + "Purged resource cache directory" + else + "Failed to purge resource cache directory" + logger.info(result) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt b/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt similarity index 74% rename from src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt rename to src/main/kotlin/app/revanced/utils/align/ZipAligner.kt index be40ba1b..568e20bd 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt +++ b/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt @@ -1,6 +1,6 @@ -package app.revanced.utils.signing.align +package app.revanced.utils.align -import app.revanced.utils.signing.align.zip.structures.ZipEntry +import app.revanced.utils.align.zip.structures.ZipEntry internal object ZipAligner { private const val DEFAULT_ALIGNMENT = 4 diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt b/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt similarity index 96% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt rename to src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt index 87f7db62..330c6898 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt @@ -1,4 +1,4 @@ -package app.revanced.utils.signing.align.zip +package app.revanced.utils.align.zip import java.io.DataInput import java.io.DataOutput diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt b/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt similarity index 75% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt rename to src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt index e64cd1a1..f961488c 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt @@ -1,7 +1,7 @@ -package app.revanced.utils.signing.align.zip +package app.revanced.utils.align.zip -import app.revanced.utils.signing.align.zip.structures.ZipEndRecord -import app.revanced.utils.signing.align.zip.structures.ZipEntry +import app.revanced.utils.align.zip.structures.ZipEndRecord +import app.revanced.utils.align.zip.structures.ZipEntry import java.io.Closeable import java.io.File import java.io.RandomAccessFile @@ -11,15 +11,15 @@ import java.util.zip.CRC32 import java.util.zip.Deflater class ZipFile(file: File) : Closeable { - var entries: MutableList = mutableListOf() + private var entries: MutableList = mutableListOf() private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") - private var CDNeedsRewrite = false + private var centralDirectoryNeedsRewrite = false private val compressionLevel = 5 init { - //if file isn't empty try to load entries + // If file isn't empty try to load entries. if (file.length() > 0) { val endRecord = findEndRecord() @@ -29,17 +29,17 @@ class ZipFile(file: File) : Closeable { entries = readEntries(endRecord).toMutableList() } - //seek back to start for writing + // Seek back to start for writing. filePointer.seek(0) } private fun findEndRecord(): ZipEndRecord { - //look from end to start since end record is at the end + // Look from end to start since end record is at the end. for (i in filePointer.length() - 1 downTo 0) { filePointer.seek(i) - //possible beginning of signature + // Possible beginning of signature. if (filePointer.readByte() == 0x50.toByte()) { - //seek back to get the full int + // Seek back to get the full int. filePointer.seek(i) val possibleSignature = filePointer.readUIntLE() if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { @@ -76,7 +76,7 @@ class ZipFile(file: File) : Closeable { } private fun writeCD() { - val CDStart = filePointer.channel.position().toUInt() + val centralDirectoryStartOffset = filePointer.channel.position().toUInt() entries.forEach { filePointer.channel.write(it.toCDE()) @@ -89,8 +89,8 @@ class ZipFile(file: File) : Closeable { 0u, entriesCount, entriesCount, - filePointer.channel.position().toUInt() - CDStart, - CDStart, + filePointer.channel.position().toUInt() - centralDirectoryStartOffset, + centralDirectoryStartOffset, "" ) @@ -98,7 +98,7 @@ class ZipFile(file: File) : Closeable { } private fun addEntry(entry: ZipEntry, data: ByteBuffer) { - CDNeedsRewrite = true + centralDirectoryNeedsRewrite = true entry.localHeaderOffset = filePointer.channel.position().toUInt() @@ -114,8 +114,7 @@ class ZipFile(file: File) : Closeable { compressor.finish() val uncompressedSize = data.size - val compressedData = - ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger + val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger. val compressedDataLength = compressor.deflate(compressedData) val compressedBuffer = @@ -126,7 +125,7 @@ class ZipFile(file: File) : Closeable { val crc = CRC32() crc.update(data) - entry.compression = 8u //deflate compression + entry.compression = 8u // Deflate compression. entry.uncompressedSize = uncompressedSize.toUInt() entry.compressedSize = compressedDataLength.toUInt() entry.crc32 = crc.value.toUInt() @@ -136,14 +135,14 @@ class ZipFile(file: File) : Closeable { private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) { alignment?.let { - //calculate where data would end up + // Calculate where data would end up. val dataOffset = filePointer.filePointer + entry.LFHSize val mod = dataOffset % alignment - //wrong alignment + // Wrong alignment. if (mod != 0L) { - //add padding at end of extra field + // Add padding at end of extra field. entry.localExtraField = entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt()) } @@ -152,7 +151,7 @@ class ZipFile(file: File) : Closeable { addEntry(entry, data) } - fun getDataForEntry(entry: ZipEntry): ByteBuffer { + private fun getDataForEntry(entry: ZipEntry): ByteBuffer { return filePointer.channel.map( FileChannel.MapMode.READ_ONLY, entry.dataOffset.toLong(), @@ -160,9 +159,15 @@ class ZipFile(file: File) : Closeable { ) } + /** + * Copies all entries from [file] to this file but skip already existing entries. + * + * @param file The file to copy entries from. + * @param entryAlignment A function that returns the alignment for a given entry. + */ fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) { for (entry in file.entries) { - if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates + if (entries.any { it.fileName == entry.fileName }) continue // Skip duplicates val data = file.getDataForEntry(entry) addEntryCopyData(entry, data, entryAlignment(entry)) @@ -170,7 +175,7 @@ class ZipFile(file: File) : Closeable { } override fun close() { - if (CDNeedsRewrite) writeCD() + if (centralDirectoryNeedsRewrite) writeCD() filePointer.close() } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt similarity index 89% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt rename to src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt index d26e551d..387679ed 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt @@ -1,9 +1,9 @@ -package app.revanced.utils.signing.align.zip.structures +package app.revanced.utils.align.zip.structures -import app.revanced.utils.signing.align.zip.putUInt -import app.revanced.utils.signing.align.zip.putUShort -import app.revanced.utils.signing.align.zip.readUIntLE -import app.revanced.utils.signing.align.zip.readUShortLE +import app.revanced.utils.align.zip.putUInt +import app.revanced.utils.align.zip.putUShort +import app.revanced.utils.align.zip.readUIntLE +import app.revanced.utils.align.zip.readUShortLE import java.io.DataInput import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt similarity index 98% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt rename to src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt index d99a73d4..316a8360 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt @@ -1,6 +1,6 @@ -package app.revanced.utils.signing.align.zip.structures +package app.revanced.utils.align.zip.structures -import app.revanced.utils.signing.align.zip.* +import app.revanced.utils.align.zip.* import java.io.DataInput import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/src/main/kotlin/app/revanced/utils/signing/Signer.kt b/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt similarity index 68% rename from src/main/kotlin/app/revanced/utils/signing/Signer.kt rename to src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt index 358395a1..a6bf3379 100644 --- a/src/main/kotlin/app/revanced/utils/signing/Signer.kt +++ b/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt @@ -1,7 +1,6 @@ package app.revanced.utils.signing -import app.revanced.cli.command.MainCommand.logger -import app.revanced.cli.signing.SigningOptions +import app.revanced.cli.command.logger import com.android.apksig.ApkSigner import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo @@ -18,10 +17,40 @@ import java.security.* import java.security.cert.X509Certificate import java.util.* -internal class Signer( +internal class ApkSigner( private val signingOptions: SigningOptions ) { + private val signer: ApkSigner.Builder private val passwordCharArray = signingOptions.password.toCharArray() + + init { + Security.addProvider(BouncyCastleProvider()) + + val keyStore = KeyStore.getInstance("BKS", "BC") + val alias = keyStore.let { store -> + FileInputStream(File(signingOptions.keyStoreFilePath).also { + if (!it.exists()) { + logger.info("Creating keystore at ${it.absolutePath}") + newKeystore(it) + } else { + logger.info("Using keystore at ${it.absolutePath}") + } + }).use { fis -> store.load(fis, null) } + store.aliases().nextElement() + } + + with( + ApkSigner.SignerConfig.Builder( + signingOptions.cn, + keyStore.getKey(alias, passwordCharArray) as PrivateKey, + listOf(keyStore.getCertificate(alias) as X509Certificate) + ).build() + ) { + this@ApkSigner.signer = ApkSigner.Builder(listOf(this)) + signer.setCreatedBy(signingOptions.cn) + } + } + private fun newKeystore(out: File) { val (publicKey, privateKey) = createKey() val privateKS = KeyStore.getInstance("BKS", "BC") @@ -50,30 +79,12 @@ internal class Signer( return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private } - fun signApk(input: File, output: File) { - Security.addProvider(BouncyCastleProvider()) - - // TODO: keystore should be saved securely - val ks = File(signingOptions.keyStoreFilePath) - if (!ks.exists()) newKeystore(ks) else { - logger.info("Found existing keystore: ${ks.name}") - } - - val keyStore = KeyStore.getInstance("BKS", "BC") - FileInputStream(ks).use { fis -> keyStore.load(fis, null) } - val alias = keyStore.aliases().nextElement() - - val config = ApkSigner.SignerConfig.Builder( - signingOptions.cn, - keyStore.getKey(alias, passwordCharArray) as PrivateKey, - listOf(keyStore.getCertificate(alias) as X509Certificate) - ).build() - - val signer = ApkSigner.Builder(listOf(config)) - signer.setCreatedBy(signingOptions.cn) + fun signApk(input: File, output: File): File { signer.setInputApk(input) signer.setOutputApk(output) signer.build().sign() + + return output } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt b/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt new file mode 100644 index 00000000..9ffdc6df --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt @@ -0,0 +1,7 @@ +package app.revanced.utils.signing + +data class SigningOptions( + val cn: String, + val password: String, + val keyStoreFilePath: String +) \ No newline at end of file