From f59ecbccd14a08d87d4f18c3c0cc47a884088b99 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 9 Mar 2024 00:56:15 +0100 Subject: [PATCH] fix: Sign APKs using `apksig` Previously, the signing extension from apkzlib was used incorrectly. The extension is meant to be added to a ZFile whereas on changes the extension would be used to sign. Instead the extension was added to a newly created ZFile, and without any changes, closed again, leading to no APK being signed. It turns out to be impractical to use the signing extension as we do not write an entire APK ZFile so that the signing extension can consider every file inside the ZFile, instead we just merge the patcher result to an existing ZFile. Instead use `apksig` after applying the patcher result to the ZFile which signs everything correctly. --- api/revanced-library.api | 4 + build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + .../kotlin/app/revanced/library/ApkSigner.kt | 94 ++++++++++++++++++- .../kotlin/app/revanced/library/ApkUtils.kt | 60 ++++++++---- 5 files changed, 141 insertions(+), 20 deletions(-) diff --git a/api/revanced-library.api b/api/revanced-library.api index c6ff752..91e2553 100644 --- a/api/revanced-library.api +++ b/api/revanced-library.api @@ -1,6 +1,8 @@ public final class app/revanced/library/ApkSigner { public static final field INSTANCE Lapp/revanced/library/ApkSigner; public final fun newApkSigner (Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer; + public final fun newApkSigner (Ljava/lang/String;Lapp/revanced/library/ApkSigner$PrivateKeyCertificatePair;)Lapp/revanced/library/ApkSigner$Signer; + public final fun newApkSigner (Ljava/lang/String;Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/library/ApkSigner$Signer; 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/Set;)V public final fun newKeyStore (Ljava/util/Set;)Ljava/security/KeyStore; @@ -27,12 +29,14 @@ public final class app/revanced/library/ApkSigner$PrivateKeyCertificatePair { public final class app/revanced/library/ApkSigner$Signer { public final fun signApk (Lcom/android/tools/build/apkzlib/zip/ZFile;)V public final fun signApk (Ljava/io/File;)V + public final fun signApk (Ljava/io/File;Ljava/io/File;)V } public final class app/revanced/library/ApkUtils { public static final field INSTANCE Lapp/revanced/library/ApkUtils; public final fun applyTo (Lapp/revanced/patcher/PatcherResult;Ljava/io/File;)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 class app/revanced/library/ApkUtils$SigningOptions { diff --git a/build.gradle.kts b/build.gradle.kts index ffcec6d..25048b2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(libs.jadb) // Fork with Shell v2 support. implementation(libs.jackson.module.kotlin) implementation(libs.apkzlib) + implementation(libs.apksig) implementation(libs.bcpkix.jdk15on) implementation(libs.guava) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e0fc88e..04a06fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ binary-compatibility-validator = "0.14.0" apkzlib = "8.3.0" bcpkix-jdk15on = "1.70" guava = "33.0.0-jre" +apksig = "8.3.0" [libraries] jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" } @@ -17,6 +18,7 @@ revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "re 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" } +apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } [plugins] binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } diff --git a/src/main/kotlin/app/revanced/library/ApkSigner.kt b/src/main/kotlin/app/revanced/library/ApkSigner.kt index 9fe0aea..dce445e 100644 --- a/src/main/kotlin/app/revanced/library/ApkSigner.kt +++ b/src/main/kotlin/app/revanced/library/ApkSigner.kt @@ -1,5 +1,6 @@ package app.revanced.library +import com.android.apksig.ApkSigner.SignerConfig import com.android.tools.build.apkzlib.sign.SigningExtension import com.android.tools.build.apkzlib.sign.SigningOptions import com.android.tools.build.apkzlib.zip.ZFile @@ -182,6 +183,7 @@ object ApkSigner { /** * Create a new [Signer]. * + * @param signer The name of the signer. * @param privateKeyCertificatePair The private key and certificate pair to use for signing. * * @return The new [Signer]. @@ -189,6 +191,38 @@ object ApkSigner { * @see PrivateKeyCertificatePair * @see Signer */ + fun newApkSigner( + signer: String, + privateKeyCertificatePair: PrivateKeyCertificatePair, + ) = Signer( + com.android.apksig.ApkSigner.Builder( + listOf( + SignerConfig.Builder( + signer, + privateKeyCertificatePair.privateKey, + listOf(privateKeyCertificatePair.certificate), + ).build(), + ), + ), + ) + + /** + * Create a new [Signer]. + * + * @param privateKeyCertificatePair The private key and certificate pair to use for signing. + * + * @return The new [Signer]. + * + * @see PrivateKeyCertificatePair + * @see Signer + */ + @Suppress("DEPRECATION") + @Deprecated( + "This method will be removed in the future.", + ReplaceWith( + "newApkSigner(\"ReVanced\", privateKeyCertificatePair)", + ), + ) fun newApkSigner(privateKeyCertificatePair: PrivateKeyCertificatePair) = Signer( SigningExtension( @@ -202,6 +236,26 @@ object ApkSigner { ), ) + /** + * Create a new [Signer]. + * + * @param signer The name of the signer. + * @param keyStore The keystore 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. + * + * @return The new [Signer]. + * + * @see KeyStore + * @see Signer + */ + fun newApkSigner( + signer: String, + keyStore: KeyStore, + keyStoreEntryAlias: String, + keyStoreEntryPassword: String, + ) = newApkSigner(signer, readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword)) + /** * Create a new [Signer]. * @@ -214,11 +268,18 @@ object ApkSigner { * @see KeyStore * @see Signer */ + @Deprecated( + "This method will be removed in the future.", + ReplaceWith( + "newApkSigner(\"ReVanced\", readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword))", + "app.revanced.library.ApkSigner.newApkSigner", + ), + ) fun newApkSigner( keyStore: KeyStore, keyStoreEntryAlias: String, keyStoreEntryPassword: String, - ) = newApkSigner(readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword)) + ) = newApkSigner("ReVanced", readKeyCertificatePair(keyStore, keyStoreEntryAlias, keyStoreEntryPassword)) /** * An entry in a keystore. @@ -246,23 +307,48 @@ object ApkSigner { val certificate: X509Certificate, ) - class Signer internal constructor(private val signingExtension: SigningExtension) { + class Signer { + private val signerBuilder: com.android.apksig.ApkSigner.Builder? + private val signingExtension: SigningExtension? + + internal constructor(signerBuilder: com.android.apksig.ApkSigner.Builder) { + this.signerBuilder = signerBuilder + signingExtension = null + } + + @Deprecated("This constructor will be removed in the future.") + internal constructor(signingExtension: SigningExtension) { + signerBuilder = null + this.signingExtension = signingExtension + } + /** * Sign an APK file. * * @param apkFile The APK file to sign. */ - fun signApk(apkFile: File) = ZFile.openReadWrite(apkFile).use { signApk(it) } + @Deprecated("This method will be removed in the future.") + fun signApk(apkFile: File) = ZFile.openReadWrite(apkFile).use { + @Suppress("DEPRECATION") + signApk(it) + } /** * Sign an APK file. * * @param apkZFile The APK [ZFile] to sign. */ + @Deprecated("This method will be removed in the future.") fun signApk(apkZFile: ZFile) { logger.info("Signing ${apkZFile.file.name}") - signingExtension.register(apkZFile) + signingExtension?.register(apkZFile) + } + + fun signApk(inputApkFile: File, outputApkFile: File) { + logger.info("Signing APK") + + signerBuilder?.setInputApk(inputApkFile)?.setOutputApk(outputApkFile)?.build()?.sign() } } } diff --git a/src/main/kotlin/app/revanced/library/ApkUtils.kt b/src/main/kotlin/app/revanced/library/ApkUtils.kt index 34bc8f9..ab73bc3 100644 --- a/src/main/kotlin/app/revanced/library/ApkUtils.kt +++ b/src/main/kotlin/app/revanced/library/ApkUtils.kt @@ -84,7 +84,7 @@ object ApkUtils { } } - logger.info("Aligning ${apkFile.name}") + logger.info("Aligning APK") targetApkZFile.realign() @@ -92,28 +92,38 @@ object ApkUtils { } } + /** + * Reads an existing or creates a new keystore. + * + * @param signingOptions The options to use for signing. + */ + private fun readOrNewKeyStore(signingOptions: SigningOptions) = if (signingOptions.keyStore.exists()) { + ApkSigner.readKeyStore( + signingOptions.keyStore.inputStream(), + signingOptions.keyStorePassword ?: "", + ) + } else { + val entry = ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password) + + // Create a new keystore with a new keypair and saves it. + ApkSigner.newKeyStore(setOf(entry)).apply { + store( + signingOptions.keyStore.outputStream(), + signingOptions.keyStorePassword?.toCharArray(), + ) + } + } + /** * Signs the apk file with the given options. * * @param signingOptions The options to use for signing. */ + @Deprecated("Use sign(File, File, SigningOptions) instead.") fun File.sign(signingOptions: SigningOptions) { - // Get the keystore from the file or create a new one. - val keyStore = - if (signingOptions.keyStore.exists()) { - ApkSigner.readKeyStore(signingOptions.keyStore.inputStream(), signingOptions.keyStorePassword ?: "") - } else { - val entries = setOf(ApkSigner.KeyStoreEntry(signingOptions.alias, signingOptions.password)) - - // Create a new keystore with a new keypair and saves it. - ApkSigner.newKeyStore(entries).apply { - store( - signingOptions.keyStore.outputStream(), - signingOptions.keyStorePassword?.toCharArray(), - ) - } - } + val keyStore = readOrNewKeyStore(signingOptions) + @Suppress("DEPRECATION") ApkSigner.newApkSigner( keyStore, signingOptions.alias, @@ -121,6 +131,24 @@ object ApkUtils { ).signApk(this) } + /** + * Signs [inputApkFile] with the given options and saves the signed apk to [outputApkFile]. + * + * @param inputApkFile The apk file to sign. + * @param outputApkFile The file to save the signed apk to. + * @param signingOptions The options to use for signing. + */ + fun sign(inputApkFile: File, outputApkFile: File, signingOptions: SigningOptions) { + val keyStore = readOrNewKeyStore(signingOptions) + + ApkSigner.newApkSigner( + signingOptions.signer, + keyStore, + signingOptions.alias, + signingOptions.password, + ).signApk(inputApkFile, outputApkFile) + } + /** * Options for signing an apk. *