From 77d91735ffbbd6e733f08176f535bfd39ece0c29 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 22 Sep 2023 02:24:09 +0200 Subject: [PATCH] feat: Add function to get the most common compatible version This adds a function to get the version that is most common for a given package name in a supplied set of patches. --- revanced-lib/api/revanced-lib.api | 59 ++++++++------- .../revanced/lib/{signing => }/ApkSigner.kt | 74 ++++++++++--------- .../main/kotlin/app/revanced/lib/ApkUtils.kt | 6 +- .../kotlin/app/revanced/lib/PatchUtils.kt | 28 +++++++ .../options => lib}/PatchOptionsTest.kt | 26 +++---- .../kotlin/app/revanced/lib/PatchUtilsTest.kt | 50 +++++++++++++ 6 files changed, 165 insertions(+), 78 deletions(-) rename revanced-lib/src/main/kotlin/app/revanced/lib/{signing => }/ApkSigner.kt (97%) create mode 100644 revanced-lib/src/main/kotlin/app/revanced/lib/PatchUtils.kt rename revanced-lib/src/test/kotlin/app/revanced/{patcher/options => lib}/PatchOptionsTest.kt (71%) create mode 100644 revanced-lib/src/test/kotlin/app/revanced/lib/PatchUtilsTest.kt diff --git a/revanced-lib/api/revanced-lib.api b/revanced-lib/api/revanced-lib.api index 685ab41b..50b5bcd5 100644 --- a/revanced-lib/api/revanced-lib.api +++ b/revanced-lib/api/revanced-lib.api @@ -1,3 +1,30 @@ +public final class app/revanced/lib/ApkSigner { + public static final field INSTANCE Lapp/revanced/lib/ApkSigner; + public final fun newApkSignerBuilder (Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; + 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 newKeyStore (Ljava/util/List;)Ljava/security/KeyStore; + public final fun newKeystore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V + public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair; + public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/lib/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair; + public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair; + 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/lib/ApkSigner$KeyStoreEntry { + public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAlias ()Ljava/lang/String; + public final fun getPassword ()Ljava/lang/String; + public final fun getPrivateKeyCertificatePair ()Lapp/revanced/lib/ApkSigner$PrivateKeyCertificatePair; +} + +public final class app/revanced/lib/ApkSigner$PrivateKeyCertificatePair { + public fun (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V + public final fun getCertificate ()Ljava/security/cert/X509Certificate; + public final fun getPrivateKey ()Ljava/security/PrivateKey; +} + public final class app/revanced/lib/ApkUtils { public static final field INSTANCE Lapp/revanced/lib/ApkUtils; public final fun copyAligned (Ljava/io/File;Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V @@ -33,6 +60,11 @@ public final class app/revanced/lib/Options$Patch$Option { public final fun getValue ()Ljava/lang/Object; } +public final class app/revanced/lib/PatchUtils { + public static final field INSTANCE Lapp/revanced/lib/PatchUtils; + public final fun getMostCommonCompatibleVersion (Ljava/util/Set;Ljava/lang/String;)Ljava/lang/String; +} + public abstract class app/revanced/lib/adb/AdbManager { public static final field Companion Lapp/revanced/lib/adb/AdbManager$Companion; public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -86,33 +118,6 @@ public final class app/revanced/lib/logging/Logger { public static synthetic fun setFormat$default (Lapp/revanced/lib/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V } -public final class app/revanced/lib/signing/ApkSigner { - public static final field INSTANCE Lapp/revanced/lib/signing/ApkSigner; - public final fun newApkSignerBuilder (Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;Ljava/lang/String;Ljava/lang/String;)Lcom/android/apksig/ApkSigner$Builder; - 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 newKeyStore (Ljava/util/List;)Ljava/security/KeyStore; - public final fun newKeystore (Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/List;)V - public final fun newPrivateKeyCertificatePair (Ljava/lang/String;Ljava/util/Date;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair; - public static synthetic fun newPrivateKeyCertificatePair$default (Lapp/revanced/lib/signing/ApkSigner;Ljava/lang/String;Ljava/util/Date;ILjava/lang/Object;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair; - public final fun readKeyCertificatePair (Ljava/security/KeyStore;Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair; - 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/lib/signing/ApkSigner$KeyStoreEntry { - public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getAlias ()Ljava/lang/String; - public final fun getPassword ()Ljava/lang/String; - public final fun getPrivateKeyCertificatePair ()Lapp/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair; -} - -public final class app/revanced/lib/signing/ApkSigner$PrivateKeyCertificatePair { - public fun (Ljava/security/PrivateKey;Ljava/security/cert/X509Certificate;)V - public final fun getCertificate ()Ljava/security/cert/X509Certificate; - public final fun getPrivateKey ()Ljava/security/PrivateKey; -} - public final class app/revanced/lib/zip/ZipFile : java/io/Closeable { public static final field ApkZipFile Lapp/revanced/lib/zip/ZipFile$ApkZipFile; public fun (Ljava/io/File;)V diff --git a/revanced-lib/src/main/kotlin/app/revanced/lib/signing/ApkSigner.kt b/revanced-lib/src/main/kotlin/app/revanced/lib/ApkSigner.kt similarity index 97% rename from revanced-lib/src/main/kotlin/app/revanced/lib/signing/ApkSigner.kt rename to revanced-lib/src/main/kotlin/app/revanced/lib/ApkSigner.kt index 4bc0033b..31f4f6e2 100644 --- a/revanced-lib/src/main/kotlin/app/revanced/lib/signing/ApkSigner.kt +++ b/revanced-lib/src/main/kotlin/app/revanced/lib/ApkSigner.kt @@ -1,4 +1,4 @@ -package app.revanced.lib.signing +package app.revanced.lib import com.android.apksig.ApkSigner import org.bouncycastle.asn1.x500.X500Name @@ -18,9 +18,12 @@ import java.util.* import java.util.logging.Logger import kotlin.time.Duration.Companion.days -@Suppress("unused", "MemberVisibilityCanBePrivate") +/** + * Utility class for writing or reading keystore files and entries as well as signing APK files. + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") object ApkSigner { - private val logger = Logger.getLogger(app.revanced.lib.signing.ApkSigner::class.java.name) + private val logger = Logger.getLogger(app.revanced.lib.ApkSigner::class.java.name) init { if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) @@ -67,6 +70,39 @@ object ApkSigner { return PrivateKeyCertificatePair(keyPair.private, certificate) } + + /** + * Read a [PrivateKeyCertificatePair] from a keystore entry. + * + * @param keyStore The keystore to read the entry from. + * @param keyStoreEntryAlias The alias of the key store entry to read. + * @param keyStoreEntryPassword The password for recovering the signing key. + * @return The read [PrivateKeyCertificatePair]. + * @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid. + */ + fun readKeyCertificatePair( + keyStore: KeyStore, + keyStoreEntryAlias: String, + keyStoreEntryPassword: String, + ): PrivateKeyCertificatePair { + logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias") + + if (!keyStore.containsAlias(keyStoreEntryAlias)) + throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias") + + // Read the private key and certificate from the keystore. + + val privateKey = try { + keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey + } catch (exception: UnrecoverableKeyException) { + throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias") + } + + val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate + + return PrivateKeyCertificatePair(privateKey, certificate) + } + /** * Create a new keystore with a new keypair. * @@ -167,38 +203,6 @@ object ApkSigner { } } - /** - * Read a [PrivateKeyCertificatePair] from a keystore entry. - * - * @param keyStore The keystore to read the entry from. - * @param keyStoreEntryAlias The alias of the key store entry to read. - * @param keyStoreEntryPassword The password for recovering the signing key. - * @return The read [PrivateKeyCertificatePair]. - * @throws IllegalArgumentException If the keystore does not contain the given alias or the password is invalid. - */ - fun readKeyCertificatePair( - keyStore: KeyStore, - keyStoreEntryAlias: String, - keyStoreEntryPassword: String, - ): PrivateKeyCertificatePair { - logger.fine("Reading key and certificate pair from keystore entry $keyStoreEntryAlias") - - if (!keyStore.containsAlias(keyStoreEntryAlias)) - throw IllegalArgumentException("Keystore does not contain alias $keyStoreEntryAlias") - - // Read the private key and certificate from the keystore. - - val privateKey = try { - keyStore.getKey(keyStoreEntryAlias, keyStoreEntryPassword.toCharArray()) as PrivateKey - } catch (exception: UnrecoverableKeyException) { - throw IllegalArgumentException("Invalid password for keystore entry $keyStoreEntryAlias") - } - - val certificate = keyStore.getCertificate(keyStoreEntryAlias) as X509Certificate - - return PrivateKeyCertificatePair(privateKey, certificate) - } - /** * Create a new [ApkSigner.Builder]. * diff --git a/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt b/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt index 06b73342..de48db55 100644 --- a/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt +++ b/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt @@ -1,7 +1,6 @@ package app.revanced.lib -import app.revanced.lib.signing.ApkSigner -import app.revanced.lib.signing.ApkSigner.signApk +import app.revanced.lib.ApkSigner.signApk import app.revanced.lib.zip.ZipFile import app.revanced.lib.zip.structures.ZipEntry import app.revanced.patcher.PatcherResult @@ -9,6 +8,9 @@ import java.io.File import java.util.logging.Logger import kotlin.io.path.deleteIfExists +/** + * Utility functions for working with apks. + */ @Suppress("MemberVisibilityCanBePrivate", "unused") object ApkUtils { private val logger = Logger.getLogger(ApkUtils::class.java.name) diff --git a/revanced-lib/src/main/kotlin/app/revanced/lib/PatchUtils.kt b/revanced-lib/src/main/kotlin/app/revanced/lib/PatchUtils.kt new file mode 100644 index 00000000..c003545c --- /dev/null +++ b/revanced-lib/src/main/kotlin/app/revanced/lib/PatchUtils.kt @@ -0,0 +1,28 @@ +package app.revanced.lib + +import app.revanced.patcher.PatchSet + +/** + * Utility functions for working with patches. + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +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. + */ + 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 +} \ No newline at end of file diff --git a/revanced-lib/src/test/kotlin/app/revanced/patcher/options/PatchOptionsTest.kt b/revanced-lib/src/test/kotlin/app/revanced/lib/PatchOptionsTest.kt similarity index 71% rename from revanced-lib/src/test/kotlin/app/revanced/patcher/options/PatchOptionsTest.kt rename to revanced-lib/src/test/kotlin/app/revanced/lib/PatchOptionsTest.kt index e45a8b48..073f397a 100644 --- a/revanced-lib/src/test/kotlin/app/revanced/patcher/options/PatchOptionsTest.kt +++ b/revanced-lib/src/test/kotlin/app/revanced/lib/PatchOptionsTest.kt @@ -1,6 +1,5 @@ -package app.revanced.patcher.options +package app.revanced.lib -import app.revanced.lib.Options import app.revanced.lib.Options.setOptions import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.patch.BytecodePatch @@ -11,16 +10,6 @@ import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder - -object PatchOptionsTestPatch : BytecodePatch(name = "PatchOptionsTestPatch") { - var key1 by stringPatchOption("key1", null, "title1", "description1") - var key2 by booleanPatchOption("key2", true, "title2", "description2") - - override fun execute(context: BytecodeContext) { - // Do nothing - } -} - @TestMethodOrder(MethodOrderer.OrderAnnotation::class) internal object PatchOptionsTest { private var patches = setOf(PatchOptionsTestPatch) @@ -36,8 +25,8 @@ internal object PatchOptionsTest { fun loadOptionsTest() { patches.setOptions(CHANGED_JSON) - assert(PatchOptionsTestPatch.key1 == "test") - assert(PatchOptionsTestPatch.key2 == false) + assert(PatchOptionsTestPatch.option1 == "test") + assert(PatchOptionsTestPatch.option2 == false) } private const val SERIALIZED_JSON = @@ -45,4 +34,13 @@ internal object PatchOptionsTest { private const val CHANGED_JSON = "[{\"patchName\":\"PatchOptionsTestPatch\",\"options\":[{\"key\":\"key1\",\"value\":\"test\"},{\"key\":\"key2\",\"value\":false}]}]" + + object PatchOptionsTestPatch : BytecodePatch(name = "PatchOptionsTestPatch") { + var option1 by stringPatchOption("key1", null, "title1", "description1") + var option2 by booleanPatchOption("key2", true, "title2", "description2") + + override fun execute(context: BytecodeContext) { + // Do nothing + } + } } \ No newline at end of file diff --git a/revanced-lib/src/test/kotlin/app/revanced/lib/PatchUtilsTest.kt b/revanced-lib/src/test/kotlin/app/revanced/lib/PatchUtilsTest.kt new file mode 100644 index 00000000..7b295290 --- /dev/null +++ b/revanced-lib/src/test/kotlin/app/revanced/lib/PatchUtilsTest.kt @@ -0,0 +1,50 @@ +package app.revanced.lib + +import app.revanced.patcher.PatchSet +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +internal object PatchUtilsTest { + @Test + fun `return 'a' because it is the most common version`() { + val patches = arrayOf("a", "a", "c", "d", "a", "b", "c", "d", "a", "b", "c", "d") + .map { version -> newPatch("some.package", version) } + .toSet() + + assertEqualsVersion("a", patches, "some.package") + } + + @Test + fun `return null because no patches were supplied`() { + assertEqualsVersion(null, emptySet(), "some.package") + } + + @Test + fun `return null because no patch is compatible with the supplied package name`() { + val patches = setOf(newPatch("other.package", "a")) + + assertEqualsVersion(null, patches, "other.package") + } + + @Test + fun `return null because no patch compatible package is constrained to a version`() { + val patches = setOf( + newPatch("other.package"), + newPatch("other.package"), + ) + + assertEqualsVersion(null, patches, "other.package") + } + + private fun assertEqualsVersion( + expected: String?, patches: PatchSet, compatiblePackageName: String + ) = assertEquals(expected, PatchUtils.getMostCommonCompatibleVersion(patches, compatiblePackageName)) + + private fun newPatch(packageName: String, vararg versions: String) = object : BytecodePatch( + compatiblePackages = setOf(CompatiblePackage(packageName, versions.toSet())) + ) { + override fun execute(context: BytecodeContext) {} + } +} \ No newline at end of file