diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8588ab0f6..bac3e0dd9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ appcompat = "1.7.0" okhttp = "5.0.0-alpha.14" retrofit = "2.11.0" guava = "33.4.0-jre" +apksig = "8.10.1" [libraries] annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } @@ -18,7 +19,7 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } guava = { module = "com.google.guava:guava", version.ref = "guava" } - +apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "apksig" } [plugins] android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/patches/api/patches.api b/patches/api/patches.api index 0c4935b4a..d29507054 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -116,6 +116,10 @@ public final class app/revanced/patches/all/misc/shortcut/sharetargets/RemoveSha public static final fun getRemoveShareTargetsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; } +public final class app/revanced/patches/all/misc/spoof/SignatureSpoofPatchKt { + public static final fun getSignatureSpoofPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + public final class app/revanced/patches/all/misc/targetSdk/SetTargetSdkVersion34Kt { public static final fun getSetTargetSdkVersion34 ()Lapp/revanced/patcher/patch/ResourcePatch; } diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts index eecadbd25..6153055b9 100644 --- a/patches/build.gradle.kts +++ b/patches/build.gradle.kts @@ -15,6 +15,9 @@ patches { dependencies { // Required due to smali, or build fails. Can be removed once smali is bumped. implementation(libs.guava) + + implementation(libs.apksig) + // Android API stubs defined here. compileOnly(project(":patches:stub")) } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SignatureSpoofPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SignatureSpoofPatch.kt new file mode 100644 index 000000000..8d32bf629 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/spoof/SignatureSpoofPatch.kt @@ -0,0 +1,95 @@ +package app.revanced.patches.all.misc.spoof + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.util.getNode +import com.android.apksig.ApkVerifier +import com.android.apksig.apk.ApkFormatException +import org.w3c.dom.Element +import java.io.ByteArrayInputStream +import java.io.IOException +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.attribute.BasicFileAttributes +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.util.* +import kotlin.io.path.Path + +val signatureSpoofPatch = resourcePatch( + name = "Spoof app signature", + description = "Spoofs the app signature via the \"fake-signature\" meta key. " + + "This patch only works with patched device roms.", + use = false, + ) { + val signature by stringOption( + key = "spoofedAppSignature", + title = "Signature", + validator = { signature -> + optionToSignature(signature) != null + }, + description = "The hex-encoded signature or path to an apk file with the desired signature", + required = true, + ) + execute { + document("AndroidManifest.xml").use { document -> + val manifest = document.getNode("manifest") as Element + + val fakeSignaturePermission = document.createElement("uses-permission") + fakeSignaturePermission.setAttribute("android:name", "android.permission.FAKE_PACKAGE_SIGNATURE") + manifest.appendChild(fakeSignaturePermission) + + val application = document.getNode("application") ?: { + val child = document.createElement("application") + manifest.appendChild(child) + child + } as Element; + + val fakeSignatureMetadata = document.createElement("meta-data") + fakeSignatureMetadata.setAttribute("android:name", "fake-signature") + fakeSignatureMetadata.setAttribute("android:value", optionToSignature(signature)) + application.appendChild(fakeSignatureMetadata) + } + } +} + +internal fun optionToSignature(signature: String?): String? { + if (signature == null) { + return null; + } + try { + // TODO: Replace with signature.hexToByteArray when stable in kotlin + val signatureBytes = HexFormat.of() + .parseHex(signature) + val factory = CertificateFactory.getInstance("X.509") + factory.generateCertificate(ByteArrayInputStream(signatureBytes)) + return signature; + } catch (_: IllegalArgumentException) { + } catch (_: CertificateException) { + } + try { + val signaturePath = Path(signature) + if (!Files.readAttributes(signaturePath, BasicFileAttributes::class.java).isRegularFile) { + return null; + } + val verifier = ApkVerifier.Builder(signaturePath.toFile()) + .build() + + val result = verifier.verify() + if (result.isVerifiedUsingV3Scheme) { + return HexFormat.of().formatHex(result.v3SchemeSigners[0].certificate.encoded) + } else if (result.isVerifiedUsingV2Scheme) { + return HexFormat.of().formatHex(result.v2SchemeSigners[0].certificate.encoded) + } else if (result.isVerifiedUsingV1Scheme) { + return HexFormat.of().formatHex(result.v1SchemeSigners[0].certificate.encoded) + } + + return null; + } catch (_: IOException) { + } catch (_: InvalidPathException) { + } catch (_: ApkFormatException) { + } catch (_: NoSuchAlgorithmException) { + } catch (_: IllegalArgumentException) {} + return null; +} \ No newline at end of file