feat: Use apkzlib instead of own implementations and bump ReVanced Patcher

BREAKING CHANGE: This commit removes deprecated APIs and bumps ReVanced Patcher. Because of it's changes, `apkzlib` is now used instead of own implementations of `ZipFile`
This commit is contained in:
oSumAtrIX
2024-02-11 21:40:16 +01:00
parent c664a6eaed
commit 3aa6dc223a
12 changed files with 173 additions and 719 deletions

3
.editorconfig Normal file
View File

@@ -0,0 +1,3 @@
[*.{kt,kts}]
ktlint_code_style = intellij_idea
ktlint_standard_no-wildcard-imports = disabled

View File

@@ -1,14 +1,13 @@
public final class app/revanced/library/ApkSigner { public final class app/revanced/library/ApkSigner {
public static final field INSTANCE Lapp/revanced/library/ApkSigner; public static final field INSTANCE Lapp/revanced/library/ApkSigner;
public final fun newApkSignerBuilder (Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; public final fun newApkSigner (Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer;
public final fun newApkSignerBuilder (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; public final fun newApkSigner (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$Signer;
public final fun newKeyStore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V public final fun newKeyStore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/Set;)V
public final fun newKeyStore (Ljava/util/List;)Ljava/security/KeyStore; public final fun newKeyStore (Ljava/util/Set;)Ljava/security/KeyStore;
public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;
public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/library/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/library/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;
public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair; public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;
public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore; public final fun readKeyStore (Ljava/io/InputStream;Ljava/lang/String;)Ljava/security/KeyStore;
public final fun signApk (Lcom/android/apksig/ApkSigner$Builder;Ljava/io/File;Ljava/io/File;)V
} }
public final class app/revanced/library/ApkSigner$KeyStoreEntry { public final class app/revanced/library/ApkSigner$KeyStoreEntry {
@@ -25,10 +24,16 @@ public final class app/revanced/library/ApkSigner$PrivateKeyCertificatePair {
public final fun getPrivateKey ()Ljava/security/PrivateKey; public final fun getPrivateKey ()Ljava/security/PrivateKey;
} }
public final class app/revanced/library/ApkSigner$Signer {
public final fun getSigningExtension ()Lcom/android/tools/build/apkzlib/sign/SigningExtension;
public final fun signApk (Lcom/android/tools/build/apkzlib/zip/ZFile;)V
public final fun signApk (Ljava/io/File;)V
}
public final class app/revanced/library/ApkUtils { public final class app/revanced/library/ApkUtils {
public static final field INSTANCE Lapp/revanced/library/ApkUtils; public static final field INSTANCE Lapp/revanced/library/ApkUtils;
public final fun copyAligned (Ljava/io/File;Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V public final fun sign (Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V
public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/library/ApkUtils$SigningOptions;)V public final fun writePatcherResult (Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V
} }
public final class app/revanced/library/ApkUtils$SigningOptions { public final class app/revanced/library/ApkUtils$SigningOptions {
@@ -162,23 +167,3 @@ public final class app/revanced/library/logging/Logger {
public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V public static synthetic fun setFormat$default (Lapp/revanced/library/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V
} }
public final class app/revanced/library/zip/ZipFile : java/io/Closeable {
public static final field ApkZipFile Lapp/revanced/library/zip/ZipFile$ApkZipFile;
public fun <init> (Ljava/io/File;)V
public final fun addEntryCompressData (Lapp/revanced/library/zip/structures/ZipEntry;[B)V
public fun close ()V
public final fun copyEntriesFromFileAligned (Lapp/revanced/library/zip/ZipFile;Lkotlin/jvm/functions/Function1;)V
}
public final class app/revanced/library/zip/ZipFile$ApkZipFile {
public final fun getApkZipEntryAlignment ()Lkotlin/jvm/functions/Function1;
}
public final class app/revanced/library/zip/structures/ZipEntry {
public static final field Companion Lapp/revanced/library/zip/structures/ZipEntry$Companion;
public fun <init> (Ljava/lang/String;)V
}
public final class app/revanced/library/zip/structures/ZipEntry$Companion {
}

View File

@@ -16,10 +16,11 @@ repositories {
dependencies { dependencies {
implementation(libs.revanced.patcher) implementation(libs.revanced.patcher)
implementation(libs.kotlin.reflect) implementation(libs.kotlin.reflect)
implementation(libs.jadb) // Updated fork implementation(libs.jadb) // Fork with Shell v2 support.
implementation(libs.apksig)
implementation(libs.bcpkix.jdk18on)
implementation(libs.jackson.module.kotlin) implementation(libs.jackson.module.kotlin)
implementation(libs.apkzlib)
implementation(libs.bcpkix.jdk15on)
implementation(libs.guava)
testImplementation(libs.revanced.patcher) testImplementation(libs.revanced.patcher)
testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test)

View File

@@ -1,21 +1,23 @@
[versions] [versions]
apksig = "8.1.4" jackson-module-kotlin = "2.15.0"
bcpkix-jdk18on = "1.76"
jackson-module-kotlin = "2.14.3"
jadb = "1.2.1" jadb = "1.2.1"
kotlin-reflect = "1.9.20" kotlin-reflect = "1.9.22"
kotlin-test = "1.9.20" kotlin-test = "1.9.22"
revanced-patcher = "19.2.0" revanced-patcher = "19.3.1"
binary-compatibility-validator = "0.13.2" binary-compatibility-validator = "0.14.0"
apkzlib = "8.2.2"
bcpkix-jdk15on = "1.70"
guava = "33.0.0-jre"
[libraries] [libraries]
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
bcpkix-jdk18on = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bcpkix-jdk18on" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" } jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" }
jadb = { module = "app.revanced:jadb", version.ref = "jadb" } jadb = { module = "app.revanced:jadb", version.ref = "jadb" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
apkzlib = { module = "com.android.tools.build:apkzlib", version.ref = "apkzlib" }
bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
[plugins] [plugins]
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }

View File

@@ -1,11 +1,12 @@
package app.revanced.library package app.revanced.library
import com.android.apksig.ApkSigner import com.android.tools.build.apkzlib.sign.SigningExtension
import com.android.tools.build.apkzlib.sign.SigningOptions
import com.android.tools.build.apkzlib.zip.ZFile
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -19,23 +20,18 @@ import java.util.logging.Logger
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
/** /**
* Utility class for writing or reading keystore files and entries as well as signing APK files. * Utility class for reading or writing keystore files and entries as well as signing APK files.
*/ */
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
object ApkSigner { object ApkSigner {
private val logger = Logger.getLogger(app.revanced.library.ApkSigner::class.java.name) private val logger = Logger.getLogger(Signer::class.java.name)
init {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(BouncyCastleProvider())
}
}
/** /**
* Create a new [PrivateKeyCertificatePair]. * Create a new [PrivateKeyCertificatePair].
* *
* @param commonName The common name of the certificate. * @param commonName The common name of the certificate.
* @param validUntil The date until the certificate is valid. * @param validUntil The date until the certificate is valid.
*
* @return The created [PrivateKeyCertificatePair]. * @return The created [PrivateKeyCertificatePair].
*/ */
fun newPrivateKeyCertificatePair( fun newPrivateKeyCertificatePair(
@@ -79,7 +75,9 @@ object ApkSigner {
* @param keyStore The keystore to read the entry from. * @param keyStore The keystore to read the entry from.
* @param keyStoreEntryAlias The alias of the key store entry to read. * @param keyStoreEntryAlias The alias of the key store entry to read.
* @param keyStoreEntryPassword The password for recovering the signing key. * @param keyStoreEntryPassword The password for recovering the signing key.
*
* @return The read [PrivateKeyCertificatePair]. * @return The read [PrivateKeyCertificatePair].
*
* @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid. * @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid.
*/ */
fun readKeyCertificatePair( fun readKeyCertificatePair(
@@ -111,13 +109,15 @@ object ApkSigner {
* Create a new keystore with a new keypair. * Create a new keystore with a new keypair.
* *
* @param entries The entries to add to the keystore. * @param entries The entries to add to the keystore.
*
* @return The created keystore. * @return The created keystore.
*
* @see KeyStoreEntry * @see KeyStoreEntry
*/ */
fun newKeyStore(entries: List<KeyStoreEntry>): KeyStore { fun newKeyStore(entries: Set<KeyStoreEntry>): KeyStore {
logger.fine("Creating keystore") logger.fine("Creating keystore")
return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply { return KeyStore.getInstance(KeyStore.getDefaultType()).apply {
load(null) load(null)
entries.forEach { entry -> entries.forEach { entry ->
@@ -142,18 +142,20 @@ object ApkSigner {
fun newKeyStore( fun newKeyStore(
keyStoreOutputStream: OutputStream, keyStoreOutputStream: OutputStream,
keyStorePassword: String, keyStorePassword: String,
entries: List<KeyStoreEntry>, entries: Set<KeyStoreEntry>,
) = newKeyStore(entries).store( ) = newKeyStore(entries).store(
keyStoreOutputStream, keyStoreOutputStream,
keyStorePassword.toCharArray(), keyStorePassword.toCharArray(),
) // Save the keystore. )
/** /**
* Read a keystore from the given [keyStoreInputStream]. * Read a keystore from the given [keyStoreInputStream].
* *
* @param keyStoreInputStream The stream to read the keystore from. * @param keyStoreInputStream The stream to read the keystore from.
* @param keyStorePassword The password for the keystore. * @param keyStorePassword The password for the keystore.
*
* @return The keystore. * @return The keystore.
*
* @throws IllegalArgumentException If the keystore password is invalid. * @throws IllegalArgumentException If the keystore password is invalid.
*/ */
fun readKeyStore( fun readKeyStore(
@@ -162,7 +164,7 @@ object ApkSigner {
): KeyStore { ): KeyStore {
logger.fine("Reading keystore") logger.fine("Reading keystore")
return KeyStore.getInstance("BKS", BouncyCastleProvider.PROVIDER_NAME).apply { return KeyStore.getInstance(KeyStore.getDefaultType()).apply {
try { try {
load(keyStoreInputStream, keyStorePassword?.toCharArray()) load(keyStoreInputStream, keyStorePassword?.toCharArray())
} catch (exception: IOException) { } catch (exception: IOException) {
@@ -176,75 +178,45 @@ object ApkSigner {
} }
/** /**
* Create a new [ApkSigner.Builder]. * Create a new [Signer].
* *
* @param privateKeyCertificatePair The private key and certificate pair to use for signing. * @param privateKeyCertificatePair The private key and certificate pair to use for signing.
* @param signer The name of the signer. *
* @param createdBy The value for the `Created-By` attribute in the APK's manifest. * @return The new [Signer].
* @return The created [ApkSigner.Builder] instance. *
* @see PrivateKeyCertificatePair
* @see Signer
*/ */
fun newApkSignerBuilder( fun newApkSigner(privateKeyCertificatePair: PrivateKeyCertificatePair) =
privateKeyCertificatePair: PrivateKeyCertificatePair, Signer(
signer: String, SigningExtension(
createdBy: String, SigningOptions.builder()
): ApkSigner.Builder { .setMinSdkVersion(21) // TODO: Extracting from the target APK would be ideal.
logger.fine( .setV1SigningEnabled(true)
"Creating new ApkSigner " + .setV2SigningEnabled(true)
"with $signer as signer and " + .setCertificates(privateKeyCertificatePair.certificate)
"$createdBy as Created-By attribute in the APK's manifest", .setKey(privateKeyCertificatePair.privateKey)
.build(),
),
) )
// Create the signer config.
val signerConfig =
ApkSigner.SignerConfig.Builder(
signer,
privateKeyCertificatePair.privateKey,
listOf(privateKeyCertificatePair.certificate),
).build()
// Create the signer.
return ApkSigner.Builder(listOf(signerConfig)).apply {
setCreatedBy(createdBy)
}
}
/** /**
* Create a new [ApkSigner.Builder]. * Create a new [Signer].
* *
* @param keyStore The keystore to use for signing. * @param keyStore The keystore to use for signing.
* @param keyStoreEntryAlias The alias of the key store entry to use for signing. * @param keyStoreEntryAlias The alias of the key store entry to use for signing.
* @param keyStoreEntryPassword The password for recovering the signing key. * @param keyStoreEntryPassword The password for recovering the signing key.
* @param signer The name of the signer. *
* @param createdBy The value for the `Created-By` attribute in the APK's manifest. * @return The new [Signer].
* @return The created [ApkSigner.Builder] instance. *
* @see KeyStoreEntry * @see KeyStore
* @see PrivateKeyCertificatePair * @see Signer
* @see ApkSigner.Builder.setCreatedBy
* @see ApkSigner.Builder.signApk
*/ */
fun newApkSignerBuilder( fun newApkSigner(
keyStore: KeyStore, keyStore: KeyStore,
keyStoreEntryAlias: String, keyStoreEntryAlias: String,
keyStoreEntryPassword: String, keyStoreEntryPassword: String,
signer: String, ) = newApkSigner(readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword))
createdBy: String,
) = newApkSignerBuilder(
readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword),
signer,
createdBy,
)
fun ApkSigner.Builder.signApk(
input: File,
output: File,
) {
logger.info("Signing ${input.name}")
setInputApk(input)
setOutputApk(output)
build().sign()
}
/** /**
* An entry in a keystore. * An entry in a keystore.
@@ -252,6 +224,7 @@ object ApkSigner {
* @param alias The alias of the entry. * @param alias The alias of the entry.
* @param password The password for recovering the signing key. * @param password The password for recovering the signing key.
* @param privateKeyCertificatePair The private key and certificate pair. * @param privateKeyCertificatePair The private key and certificate pair.
*
* @see PrivateKeyCertificatePair * @see PrivateKeyCertificatePair
*/ */
class KeyStoreEntry( class KeyStoreEntry(
@@ -270,4 +243,24 @@ object ApkSigner {
val privateKey: PrivateKey, val privateKey: PrivateKey,
val certificate: X509Certificate, val certificate: X509Certificate,
) )
class Signer internal constructor(val signingExtension: SigningExtension) {
/**
* Sign an APK file.
*
* @param apkFile The APK file to sign.
*/
fun signApk(apkFile: File) = ZFile.openReadWrite(apkFile).use { signApk(it) }
/**
* Sign an APK file.
*
* @param apkZFile The APK [ZFile] to sign.
*/
fun signApk(apkZFile: ZFile) {
logger.info("Signing ${apkZFile.file.name}")
signingExtension.register(apkZFile)
}
}
} }

View File

@@ -1,96 +1,124 @@
package app.revanced.library package app.revanced.library
import app.revanced.library.ApkSigner.signApk
import app.revanced.library.zip.ZipFile
import app.revanced.library.zip.structures.ZipEntry
import app.revanced.patcher.PatcherResult import app.revanced.patcher.PatcherResult
import com.android.tools.build.apkzlib.zip.AlignmentRules
import com.android.tools.build.apkzlib.zip.StoredEntry
import com.android.tools.build.apkzlib.zip.ZFile
import com.android.tools.build.apkzlib.zip.ZFileOptions
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.io.path.deleteIfExists
/** /**
* Utility functions for working with apks. * Utility functions to work with APK files.
*/ */
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
object ApkUtils { object ApkUtils {
private val logger = Logger.getLogger(ApkUtils::class.java.name) private val logger = Logger.getLogger(ApkUtils::class.java.name)
private const val LIBRARY_EXTENSION = ".so"
// Alignment for native libraries.
private const val LIBRARY_ALIGNMENT = 1024 * 4
// Alignment for all other files.
private const val DEFAULT_ALIGNMENT = 4
// Prefix for resources.
private const val RES_PREFIX = "res/"
private val zFileOptions =
ZFileOptions().setAlignmentRule(
AlignmentRules.compose(
AlignmentRules.constantForSuffix(LIBRARY_EXTENSION, LIBRARY_ALIGNMENT),
AlignmentRules.constant(DEFAULT_ALIGNMENT),
),
)
/** /**
* Creates a new apk from [apkFile] and [patchedEntriesSource] and writes it to [outputFile]. * Applies the [PatcherResult] to the given [apkFile].
* *
* @param apkFile The apk to copy entries from. * The order of operation is as follows:
* @param outputFile The apk to write the new entries to. * 1. Write patched dex files.
* @param patchedEntriesSource The result of the patcher to add the patched dex files and resources. * 2. Delete all resources in the target APK
* 3. Merge resources.apk compiled by AAPT.
* 4. Write raw resources.
* 5. Delete resources staged for deletion.
* 6. Realign the APK.
*
* @param apkFile The file to apply the patched files to.
*/ */
fun copyAligned( fun PatcherResult.applyTo(apkFile: File) {
apkFile: File, ZFile.openReadWrite(apkFile, zFileOptions).use { targetApkZFile ->
outputFile: File, dexFiles.forEach { dexFile ->
patchedEntriesSource: PatcherResult, targetApkZFile.add(dexFile.name, dexFile.stream)
) {
logger.info("Aligning ${apkFile.name}")
outputFile.toPath().deleteIfExists()
ZipFile(outputFile).use { file ->
patchedEntriesSource.dexFiles.forEach {
file.addEntryCompressData(
ZipEntry(it.name),
it.stream.readBytes(),
)
} }
patchedEntriesSource.resourceFile?.let { resources?.let { resources ->
file.copyEntriesFromFileAligned( // Add resources compiled by AAPT.
ZipFile(it), resources.resourcesApk?.let { resourcesApk ->
ZipFile.apkZipEntryAlignment, ZFile.openReadOnly(resourcesApk).use { resourcesApkZFile ->
) // Delete all resources in the target APK before merging the new ones.
// This is necessary because the resources.apk renames resources.
// So unless, the old resources are deleted, there will be orphaned resources in the APK.
// It is not necessary, but for the sake of cleanliness, it is done.
targetApkZFile.entries().filter { entry ->
entry.centralDirectoryHeader.name.startsWith(RES_PREFIX)
}.forEach(StoredEntry::delete)
targetApkZFile.mergeFrom(resourcesApkZFile) { false }
}
}
// Add resources not compiled by AAPT.
resources.otherResources?.let { otherResources ->
targetApkZFile.addAllRecursively(otherResources) { file ->
file.relativeTo(otherResources).invariantSeparatorsPath !in resources.doNotCompress
}
}
// Delete resources that were staged for deletion.
if (resources.deleteResources.isNotEmpty()) {
targetApkZFile.entries().filter { entry ->
resources.deleteResources.any { shouldDelete -> shouldDelete(entry.centralDirectoryHeader.name) }
}.forEach(StoredEntry::delete)
}
} }
// TODO: Do not compress result.doNotCompress logger.info("Aligning APK")
// TODO: Fix copying resources that are not needed anymore. targetApkZFile.realign()
file.copyEntriesFromFileAligned(
ZipFile(apkFile), logger.fine("Writing changes")
ZipFile.apkZipEntryAlignment,
)
} }
} }
/** /**
* Signs the [apk] file and writes it to [output]. * Signs the apk file with the given options.
* *
* @param apk The apk to sign.
* @param output The apk to write the signed apk to.
* @param signingOptions The options to use for signing. * @param signingOptions The options to use for signing.
*/ */
fun sign( fun File.sign(signingOptions: SigningOptions) {
apk: File,
output: File,
signingOptions: SigningOptions,
) {
// Get the keystore from the file or create a new one. // Get the keystore from the file or create a new one.
val keyStore = val keyStore =
if (signingOptions.keyStore.exists()) { if (signingOptions.keyStore.exists()) {
ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword) ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword ?: "")
} else { } else {
val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password) val entries = setOf(ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password))
// Create a new keystore with a new keypair and saves it. // Create a new keystore with a new keypair and saves it.
ApkSigner.newKeyStore(listOf(entry)).also { keyStore -> ApkSigner.newKeyStore(entries).apply {
keyStore.store( store(
signingOptions.keyStore.outputStream(), signingOptions.keyStore.outputStream(),
signingOptions.keyStorePassword?.toCharArray(), signingOptions.keyStorePassword?.toCharArray(),
) )
} }
} }
ApkSigner.newApkSignerBuilder( ApkSigner.newApkSigner(
keyStore, keyStore,
signingOptions.alias, signingOptions.alias,
signingOptions.password, signingOptions.password,
signingOptions.signer, ).signApk(this)
signingOptions.signer,
).signApk(apk, output)
} }
/** /**

View File

@@ -20,35 +20,6 @@ typealias PackageNameMap = Map<PackageName, VersionMap>
*/ */
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
object PatchUtils { object PatchUtils {
/**
* Get the version that is most common for [packageName] in the supplied set of [patches].
*
* @param patches The set of patches to check.
* @param packageName The name of the compatible package.
* @return The most common version of.
*/
@Deprecated(
"Use getMostCommonCompatibleVersions instead.",
ReplaceWith(
"getMostCommonCompatibleVersions(patches, setOf(packageName))" +
".entries.firstOrNull()?.value?.keys?.firstOrNull()",
),
)
fun getMostCommonCompatibleVersion(
patches: PatchSet,
packageName: String,
) = patches
.mapNotNull {
// Map all patches to their compatible packages with version constraints.
it.compatiblePackages?.firstOrNull { compatiblePackage ->
compatiblePackage.name == packageName && compatiblePackage.versions?.isNotEmpty() == true
}
}
.flatMap { it.versions!! }
.groupingBy { it }
.eachCount()
.maxByOrNull { it.value }?.key
/** /**
* Get the count of versions for each compatible package from a supplied set of [patches] ordered by the most common version. * Get the count of versions for each compatible package from a supplied set of [patches] ordered by the most common version.
* *

View File

@@ -1,42 +0,0 @@
package app.revanced.library.zip
import java.io.DataInput
import java.io.DataOutput
import java.nio.ByteBuffer
internal fun UInt.toLittleEndian() =
(((this.toInt() and 0xff000000.toInt()) shr 24) or ((this.toInt() and 0x00ff0000) shr 8) or ((this.toInt() and 0x0000ff00) shl 8) or (this.toInt() shl 24)).toUInt()
internal fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
internal fun UInt.toBigEndian() =
(
((this.toInt() and 0xff) shl 24) or ((this.toInt() and 0xff00) shl 8)
or ((this.toInt() and 0x00ff0000) ushr 8) or (this.toInt() ushr 24)
).toUInt()
internal fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
internal fun ByteBuffer.getUShort() = this.getShort().toUShort()
internal fun ByteBuffer.getUInt() = this.getInt().toUInt()
internal fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort())
internal fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt())
internal fun DataInput.readUShort() = this.readShort().toUShort()
internal fun DataInput.readUInt() = this.readInt().toUInt()
internal fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
internal fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
internal fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
internal fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
internal fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
internal fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())

View File

@@ -1,218 +0,0 @@
package app.revanced.library.zip
import app.revanced.library.zip.structures.ZipEndRecord
import app.revanced.library.zip.structures.ZipEntry
import java.io.Closeable
import java.io.File
import java.io.RandomAccessFile
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.util.zip.CRC32
import java.util.zip.Deflater
class ZipFile(file: File) : Closeable {
private var entries: MutableList<ZipEntry> = mutableListOf()
// Open file for writing if it doesn't exist (because the intention is to write) or is writable.
private val filePointer: RandomAccessFile =
RandomAccessFile(
file,
if (!file.exists() || file.canWrite()) "rw" else "r",
)
private var centralDirectoryNeedsRewrite = false
private val compressionLevel = 5
init {
// If file isn't empty try to load entries.
if (file.length() > 0) {
val endRecord = findEndRecord()
if (endRecord.diskNumber > 0u || endRecord.totalEntries != endRecord.diskEntries) {
throw IllegalArgumentException("Multi-file archives are not supported")
}
entries = readEntries(endRecord).toMutableList()
}
// 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.
for (i in filePointer.length() - 1 downTo 0) {
filePointer.seek(i)
// Possible beginning of signature.
if (filePointer.readByte() == 0x50.toByte()) {
// Seek back to get the full int.
filePointer.seek(i)
val possibleSignature = filePointer.readUIntLE()
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
filePointer.seek(i)
return ZipEndRecord.fromECD(filePointer)
}
}
}
throw Exception("Couldn't find end record")
}
private fun readEntries(endRecord: ZipEndRecord): List<ZipEntry> {
filePointer.seek(endRecord.centralDirectoryStartOffset.toLong())
val numberOfEntries = endRecord.diskEntries.toInt()
return buildList(numberOfEntries) {
for (i in 1..numberOfEntries) {
add(
ZipEntry.fromCDE(filePointer).also
{
// for some reason the local extra field can be different from the central one
it.readLocalExtra(
filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
it.localHeaderOffset.toLong() + 28,
2,
),
)
},
)
}
}
}
private fun writeCD() {
val centralDirectoryStartOffset = filePointer.channel.position().toUInt()
entries.forEach {
filePointer.channel.write(it.toCDE())
}
val entriesCount = entries.size.toUShort()
val endRecord =
ZipEndRecord(
0u,
0u,
entriesCount,
entriesCount,
filePointer.channel.position().toUInt() - centralDirectoryStartOffset,
centralDirectoryStartOffset,
"",
)
filePointer.channel.write(endRecord.toECD())
}
private fun addEntry(
entry: ZipEntry,
data: ByteBuffer,
) {
centralDirectoryNeedsRewrite = true
entry.localHeaderOffset = filePointer.channel.position().toUInt()
filePointer.channel.write(entry.toLFH())
filePointer.channel.write(data)
entries.add(entry)
}
fun addEntryCompressData(
entry: ZipEntry,
data: ByteArray,
) {
val compressor = Deflater(compressionLevel, true)
compressor.setInput(data)
compressor.finish()
val uncompressedSize = data.size
val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger.
val compressedDataLength = compressor.deflate(compressedData)
val compressedBuffer =
ByteBuffer.wrap(compressedData.take(compressedDataLength).toByteArray())
compressor.end()
val crc = CRC32()
crc.update(data)
entry.compression = 8u // Deflate compression.
entry.uncompressedSize = uncompressedSize.toUInt()
entry.compressedSize = compressedDataLength.toUInt()
entry.crc32 = crc.value.toUInt()
addEntry(entry, compressedBuffer)
}
private fun addEntryCopyData(
entry: ZipEntry,
data: ByteBuffer,
alignment: Int? = null,
) {
alignment?.let {
// Calculate where data would end up.
val dataOffset = filePointer.filePointer + entry.LFHSize
val mod = dataOffset % alignment
// Wrong alignment.
if (mod != 0L) {
// Add padding at end of extra field.
entry.localExtraField =
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
}
}
addEntry(entry, data)
}
private fun getDataForEntry(entry: ZipEntry): ByteBuffer {
return filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
entry.dataOffset.toLong(),
entry.compressedSize.toLong(),
)
}
/**
* 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 // Skip duplicates
val data = file.getDataForEntry(entry)
addEntryCopyData(entry, data, entryAlignment(entry))
}
}
override fun close() {
if (centralDirectoryNeedsRewrite) writeCD()
filePointer.close()
}
companion object ApkZipFile {
private const val DEFAULT_ALIGNMENT = 4
private const val LIBRARY_ALIGNMENT = 4096
val apkZipEntryAlignment = { entry: ZipEntry ->
if (entry.compression.toUInt() != 0u) {
null
} else if (entry.fileName.endsWith(".so")) {
LIBRARY_ALIGNMENT
} else {
DEFAULT_ALIGNMENT
}
}
}
}

View File

@@ -1,77 +0,0 @@
package app.revanced.library.zip.structures
import app.revanced.library.zip.putUInt
import app.revanced.library.zip.putUShort
import app.revanced.library.zip.readUIntLE
import app.revanced.library.zip.readUShortLE
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
internal class ZipEndRecord(
val diskNumber: UShort,
val startingDiskNumber: UShort,
val diskEntries: UShort,
val totalEntries: UShort,
val centralDirectorySize: UInt,
val centralDirectoryStartOffset: UInt,
val fileComment: String,
) {
companion object {
const val ECD_HEADER_SIZE = 22
const val ECD_SIGNATURE = 0x06054b50u
fun fromECD(input: DataInput): ZipEndRecord {
val signature = input.readUIntLE()
if (signature != ECD_SIGNATURE) {
throw IllegalArgumentException("Input doesn't start with end record signature")
}
val diskNumber = input.readUShortLE()
val startingDiskNumber = input.readUShortLE()
val diskEntries = input.readUShortLE()
val totalEntries = input.readUShortLE()
val centralDirectorySize = input.readUIntLE()
val centralDirectoryStartOffset = input.readUIntLE()
val fileCommentLength = input.readUShortLE()
var fileComment = ""
if (fileCommentLength > 0u) {
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
input.readFully(fileCommentBytes)
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
}
return ZipEndRecord(
diskNumber,
startingDiskNumber,
diskEntries,
totalEntries,
centralDirectorySize,
centralDirectoryStartOffset,
fileComment,
)
}
}
fun toECD(): ByteBuffer {
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
val buffer = ByteBuffer.allocate(ECD_HEADER_SIZE + commentBytes.size).also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(ECD_SIGNATURE)
buffer.putUShort(diskNumber)
buffer.putUShort(startingDiskNumber)
buffer.putUShort(diskEntries)
buffer.putUShort(totalEntries)
buffer.putUInt(centralDirectorySize)
buffer.putUInt(centralDirectoryStartOffset)
buffer.putUShort(commentBytes.size.toUShort())
buffer.put(commentBytes)
buffer.flip()
return buffer
}
}

View File

@@ -1,192 +0,0 @@
package app.revanced.library.zip.structures
import app.revanced.library.zip.*
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
class ZipEntry private constructor(
internal val version: UShort,
internal val versionNeeded: UShort,
internal val flags: UShort,
internal var compression: UShort,
internal val modificationTime: UShort,
internal val modificationDate: UShort,
internal var crc32: UInt,
internal var compressedSize: UInt,
internal var uncompressedSize: UInt,
internal val diskNumber: UShort,
internal val internalAttributes: UShort,
internal val externalAttributes: UInt,
internal var localHeaderOffset: UInt,
internal val fileName: String,
internal val extraField: ByteArray,
internal val fileComment: String,
internal var localExtraField: ByteArray = ByteArray(0), // separate for alignment
) {
internal val LFHSize: Int
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
internal val dataOffset: UInt
get() = localHeaderOffset + LFHSize.toUInt()
constructor(fileName: String) : this(
0x1403u, // made by unix, version 20
0u,
0u,
0u,
0x0821u, // seems to be static time google uses, no idea
0x0221u, // same as above
0u,
0u,
0u,
0u,
0u,
0u,
0u,
fileName,
ByteArray(0),
"",
)
companion object {
internal const val CDE_HEADER_SIZE = 46
internal const val CDE_SIGNATURE = 0x02014b50u
internal const val LFH_HEADER_SIZE = 30
internal const val LFH_SIGNATURE = 0x04034b50u
internal fun fromCDE(input: DataInput): ZipEntry {
val signature = input.readUIntLE()
if (signature != CDE_SIGNATURE) {
throw IllegalArgumentException("Input doesn't start with central directory entry signature")
}
val version = input.readUShortLE()
val versionNeeded = input.readUShortLE()
var flags = input.readUShortLE()
val compression = input.readUShortLE()
val modificationTime = input.readUShortLE()
val modificationDate = input.readUShortLE()
val crc32 = input.readUIntLE()
val compressedSize = input.readUIntLE()
val uncompressedSize = input.readUIntLE()
val fileNameLength = input.readUShortLE()
var fileName = ""
val extraFieldLength = input.readUShortLE()
val extraField = ByteArray(extraFieldLength.toInt())
val fileCommentLength = input.readUShortLE()
var fileComment = ""
val diskNumber = input.readUShortLE()
val internalAttributes = input.readUShortLE()
val externalAttributes = input.readUIntLE()
val localHeaderOffset = input.readUIntLE()
val variableFieldsLength =
fileNameLength.toInt() + extraFieldLength.toInt() + fileCommentLength.toInt()
if (variableFieldsLength > 0) {
val fileNameBytes = ByteArray(fileNameLength.toInt())
input.readFully(fileNameBytes)
fileName = fileNameBytes.toString(Charsets.UTF_8)
input.readFully(extraField)
val fileCommentBytes = ByteArray(fileCommentLength.toInt())
input.readFully(fileCommentBytes)
fileComment = fileCommentBytes.toString(Charsets.UTF_8)
}
flags = (
flags and
0b1000u.inv()
.toUShort()
) // disable data descriptor flag as they are not used
return ZipEntry(
version,
versionNeeded,
flags,
compression,
modificationTime,
modificationDate,
crc32,
compressedSize,
uncompressedSize,
diskNumber,
internalAttributes,
externalAttributes,
localHeaderOffset,
fileName,
extraField,
fileComment,
)
}
}
internal fun readLocalExtra(buffer: ByteBuffer) {
buffer.order(ByteOrder.LITTLE_ENDIAN)
localExtraField = ByteArray(buffer.getUShort().toInt())
}
internal fun toLFH(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val buffer =
ByteBuffer.allocate(LFH_HEADER_SIZE + nameBytes.size + localExtraField.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(LFH_SIGNATURE)
buffer.putUShort(versionNeeded)
buffer.putUShort(flags)
buffer.putUShort(compression)
buffer.putUShort(modificationTime)
buffer.putUShort(modificationDate)
buffer.putUInt(crc32)
buffer.putUInt(compressedSize)
buffer.putUInt(uncompressedSize)
buffer.putUShort(nameBytes.size.toUShort())
buffer.putUShort(localExtraField.size.toUShort())
buffer.put(nameBytes)
buffer.put(localExtraField)
buffer.flip()
return buffer
}
internal fun toCDE(): ByteBuffer {
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
val commentBytes = fileComment.toByteArray(Charsets.UTF_8)
val buffer =
ByteBuffer.allocate(CDE_HEADER_SIZE + nameBytes.size + extraField.size + commentBytes.size)
.also { it.order(ByteOrder.LITTLE_ENDIAN) }
buffer.putUInt(CDE_SIGNATURE)
buffer.putUShort(version)
buffer.putUShort(versionNeeded)
buffer.putUShort(flags)
buffer.putUShort(compression)
buffer.putUShort(modificationTime)
buffer.putUShort(modificationDate)
buffer.putUInt(crc32)
buffer.putUInt(compressedSize)
buffer.putUInt(uncompressedSize)
buffer.putUShort(nameBytes.size.toUShort())
buffer.putUShort(extraField.size.toUShort())
buffer.putUShort(commentBytes.size.toUShort())
buffer.putUShort(diskNumber)
buffer.putUShort(internalAttributes)
buffer.putUInt(externalAttributes)
buffer.putUInt(localHeaderOffset)
buffer.put(nameBytes)
buffer.put(extraField)
buffer.put(commentBytes)
buffer.flip()
return buffer
}
}

View File

@@ -37,7 +37,7 @@ internal object PatchOptionsTest {
"[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]" "[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]"
@Patch("PatchOptionsTestPatch") @Patch("PatchOptionsTestPatch")
object PatchOptionsTestPatch : BytecodePatch() { object PatchOptionsTestPatch : BytecodePatch(emptySet()) {
var option1 by stringPatchOption("key1", null, null, "title1", "description1") var option1 by stringPatchOption("key1", null, null, "title1", "description1")
var option2 by booleanPatchOption("key2", true, null, "title2", "description2") var option2 by booleanPatchOption("key2", true, null, "title2", "description2")