chore: switch to revanced library and bump patcher (#1314)

This commit is contained in:
Ax333l 2023-10-05 17:36:33 +02:00 committed by GitHub
parent f78b56ef0a
commit e232044157
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 171 additions and 778 deletions

View file

@ -131,11 +131,8 @@ dependencies {
ksp(libs.room.compiler) ksp(libs.room.compiler)
// ReVanced // ReVanced
implementation(libs.patcher) implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
// Signing
implementation(libs.apksign)
implementation(libs.bcpkix.jdk18on)
implementation(libs.libsu.core) implementation(libs.libsu.core)
implementation(libs.libsu.service) implementation(libs.libsu.service)

View file

@ -2,19 +2,19 @@ package app.revanced.manager.domain.manager
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import app.revanced.manager.util.signing.Signer import app.revanced.library.ApkSigner
import app.revanced.manager.util.signing.SigningOptions import app.revanced.library.ApkUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.security.UnrecoverableKeyException
import java.nio.file.StandardCopyOption
import kotlin.io.path.exists
class KeystoreManager(app: Application, private val prefs: PreferencesManager) { class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
companion object { companion object Constants {
/** /**
* Default alias and password for the keystore. * Default alias and password for the keystore.
*/ */
@ -22,37 +22,55 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
} }
private val keystorePath = private val keystorePath =
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath() app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore")
private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit { private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit {
prefs.keystoreCommonName.value = cn prefs.keystoreCommonName.value = cn
prefs.keystorePass.value = pass prefs.keystorePass.value = pass
} }
private suspend fun signingOptions(path: File = keystorePath) = ApkUtils.SigningOptions(
keyStore = path,
keyStorePassword = null,
alias = prefs.keystoreCommonName.get(),
signer = prefs.keystoreCommonName.get(),
password = prefs.keystorePass.get()
)
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) { suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
Signer( ApkUtils.sign(input, output, signingOptions())
SigningOptions(
prefs.keystoreCommonName.get(),
prefs.keystorePass.get(),
keystorePath
)
).signApk(
input,
output
)
} }
suspend fun regenerate() = withContext(Dispatchers.Default) { suspend fun regenerate() = withContext(Dispatchers.Default) {
Signer(SigningOptions(DEFAULT, DEFAULT, keystorePath)).regenerateKeystore() val ks = ApkSigner.newKeyStore(
listOf(
ApkSigner.KeyStoreEntry(
DEFAULT, DEFAULT
)
)
)
keystorePath.outputStream().use {
ks.store(it, null)
}
updatePrefs(DEFAULT, DEFAULT) updatePrefs(DEFAULT, DEFAULT)
} }
suspend fun import(cn: String, pass: String, keystore: Path): Boolean { suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
if (!Signer(SigningOptions(cn, pass, keystore)).canUnlock()) { val keystoreData = keystore.readBytes()
try {
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
ApkSigner.readKeyCertificatePair(ks, cn, pass)
} catch (_: UnrecoverableKeyException) {
return false
} catch (_: IllegalArgumentException) {
return false return false
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING) Files.write(keystorePath.toPath(), keystoreData)
} }
updatePrefs(cn, pass) updatePrefs(cn, pass)
@ -63,7 +81,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
suspend fun export(target: OutputStream) { suspend fun export(target: OutputStream) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Files.copy(keystorePath, target) Files.copy(keystorePath.toPath(), target)
} }
} }
} }

View file

@ -1,38 +0,0 @@
package app.revanced.manager.patcher
import app.revanced.manager.patcher.alignment.ZipAligner
import app.revanced.manager.patcher.alignment.zip.ZipFile
import app.revanced.manager.patcher.alignment.zip.structures.ZipEntry
import app.revanced.patcher.PatcherResult
import java.io.File
// This is the same aligner used by the CLI.
// It will be removed eventually.
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, readonly = true),
ZipAligner::getEntryAlignment
)
}
}
}

View file

@ -1,9 +1,10 @@
package app.revanced.manager.patcher package app.revanced.manager.patcher
import app.revanced.library.ApkUtils
import app.revanced.manager.ui.viewmodel.ManagerLogger import app.revanced.manager.ui.viewmodel.ManagerLogger
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.patch.PatchClass import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchResult import app.revanced.patcher.patch.PatchResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -13,7 +14,7 @@ import java.nio.file.Files
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import java.util.logging.Logger import java.util.logging.Logger
internal typealias PatchList = List<PatchClass> internal typealias PatchList = List<Patch<*>>
class Session( class Session(
cacheDir: String, cacheDir: String,
@ -69,7 +70,8 @@ class Session(
logger.info("Writing patched files...") logger.info("Writing patched files...")
val result = patcher.get() val result = patcher.get()
val aligned = temporary.resolve("aligned.apk").also { Aligning.align(result, input, it) } val aligned = temporary.resolve("aligned.apk")
ApkUtils.copyAligned(input, aligned, result)
logger.info("Patched apk saved to $aligned") logger.info("Patched apk saved to $aligned")
@ -85,7 +87,7 @@ class Session(
} }
companion object { companion object {
operator fun PatchResult.component1() = patchName operator fun PatchResult.component1() = patch.name
operator fun PatchResult.component2() = exception operator fun PatchResult.component2() = exception
} }
} }

View file

@ -1,11 +0,0 @@
package app.revanced.manager.patcher.alignment
import app.revanced.manager.patcher.alignment.zip.structures.ZipEntry
internal object ZipAligner {
private const val DEFAULT_ALIGNMENT = 4
private const val LIBRARY_ALIGNMENT = 4096
fun getEntryAlignment(entry: ZipEntry): Int? =
if (entry.compression.toUInt() != 0u) null else if (entry.fileName.endsWith(".so")) LIBRARY_ALIGNMENT else DEFAULT_ALIGNMENT
}

View file

@ -1,33 +0,0 @@
package app.revanced.manager.patcher.alignment.zip
import java.io.DataInput
import java.io.DataOutput
import java.nio.ByteBuffer
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()
fun UShort.toLittleEndian() = (this.toUInt() shl 16).toLittleEndian().toUShort()
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()
fun UShort.toBigEndian() = (this.toUInt() shl 16).toBigEndian().toUShort()
fun ByteBuffer.getUShort() = this.getShort().toUShort()
fun ByteBuffer.getUInt() = this.getInt().toUInt()
fun ByteBuffer.putUShort(ushort: UShort) = this.putShort(ushort.toShort())
fun ByteBuffer.putUInt(uint: UInt) = this.putInt(uint.toInt())
fun DataInput.readUShort() = this.readShort().toUShort()
fun DataInput.readUInt() = this.readInt().toUInt()
fun DataOutput.writeUShort(ushort: UShort) = this.writeShort(ushort.toInt())
fun DataOutput.writeUInt(uint: UInt) = this.writeInt(uint.toInt())
fun DataInput.readUShortLE() = this.readUShort().toBigEndian()
fun DataInput.readUIntLE() = this.readUInt().toBigEndian()
fun DataOutput.writeUShortLE(ushort: UShort) = this.writeUShort(ushort.toLittleEndian())
fun DataOutput.writeUIntLE(uint: UInt) = this.writeUInt(uint.toLittleEndian())

View file

@ -1,188 +0,0 @@
package app.revanced.manager.patcher.alignment.zip
import app.revanced.manager.patcher.alignment.zip.structures.ZipEndRecord
import app.revanced.manager.patcher.alignment.zip.structures.ZipEntry
import java.io.Closeable
import java.io.File
import java.io.IOException
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, private val readonly: Boolean = false) : Closeable {
var entries: MutableList<ZipEntry> = mutableListOf()
private val filePointer: RandomAccessFile = RandomAccessFile(file, if (readonly) "r" else "rw")
private var CDNeedsRewrite = 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 assertWritable() {
if (readonly) throw IOException("Archive is read-only")
}
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 CDStart = 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() - CDStart,
CDStart,
""
)
filePointer.channel.write(endRecord.toECD())
}
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
CDNeedsRewrite = 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) {
assertWritable()
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) {
assertWritable()
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)
}
fun getDataForEntry(entry: ZipEntry): ByteBuffer {
return filePointer.channel.map(
FileChannel.MapMode.READ_ONLY,
entry.dataOffset.toLong(),
entry.compressedSize.toLong()
)
}
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
assertWritable()
for (entry in file.entries) {
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates
val data = file.getDataForEntry(entry)
addEntryCopyData(entry, data, entryAlignment(entry))
}
}
override fun close() {
if (CDNeedsRewrite) writeCD()
filePointer.close()
}
}

View file

@ -1,77 +0,0 @@
package app.revanced.manager.patcher.alignment.zip.structures
import app.revanced.manager.patcher.alignment.zip.putUInt
import app.revanced.manager.patcher.alignment.zip.putUShort
import app.revanced.manager.patcher.alignment.zip.readUIntLE
import app.revanced.manager.patcher.alignment.zip.readUShortLE
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
data 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,189 +0,0 @@
package app.revanced.manager.patcher.alignment.zip.structures
import app.revanced.manager.patcher.alignment.zip.*
import java.io.DataInput
import java.nio.ByteBuffer
import java.nio.ByteOrder
data class ZipEntry(
val version: UShort,
val versionNeeded: UShort,
val flags: UShort,
var compression: UShort,
val modificationTime: UShort,
val modificationDate: UShort,
var crc32: UInt,
var compressedSize: UInt,
var uncompressedSize: UInt,
val diskNumber: UShort,
val internalAttributes: UShort,
val externalAttributes: UInt,
var localHeaderOffset: UInt,
val fileName: String,
val extraField: ByteArray,
val fileComment: String,
var localExtraField: ByteArray = ByteArray(0), //separate for alignment
) {
val LFHSize: Int
get() = LFH_HEADER_SIZE + fileName.toByteArray(Charsets.UTF_8).size + localExtraField.size
val dataOffset: UInt
get() = localHeaderOffset + LFHSize.toUInt()
companion object {
const val CDE_HEADER_SIZE = 46
const val CDE_SIGNATURE = 0x02014b50u
const val LFH_HEADER_SIZE = 30
const val LFH_SIGNATURE = 0x04034b50u
fun createWithName(fileName: String): ZipEntry {
return ZipEntry(
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),
""
)
}
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,
)
}
}
fun readLocalExtra(buffer: ByteBuffer) {
buffer.order(ByteOrder.LITTLE_ENDIAN)
localExtraField = ByteArray(buffer.getUShort().toInt())
}
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
}
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

@ -3,16 +3,15 @@ package app.revanced.manager.patcher.patch
import android.util.Log import android.util.Log
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchClass
import java.io.File import java.io.File
class PatchBundle(private val loader: Iterable<PatchClass>, val integrations: File?) { class PatchBundle(private val loader: Iterable<Patch<*>>, val integrations: File?) {
constructor(bundleJar: File, integrations: File?) : this( constructor(bundleJar: File, integrations: File?) : this(
object : Iterable<PatchClass> { object : Iterable<Patch<*>> {
private fun load(): List<PatchClass> = PatchBundleLoader.Dex(bundleJar) private fun load(): Iterable<Patch<*>> = PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null)
override fun iterator() = load().iterator() override fun iterator(): Iterator<Patch<*>> = load().iterator()
}, },
integrations integrations
) { ) {

View file

@ -1,50 +1,70 @@
package app.revanced.manager.patcher.patch package app.revanced.manager.patcher.patch
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import app.revanced.patcher.annotation.Package import app.revanced.patcher.patch.Patch
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages import app.revanced.patcher.patch.options.PatchOption
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.include
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.PatchClass
import app.revanced.patcher.patch.PatchOption
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
data class PatchInfo( data class PatchInfo(
val name: String, val name: String,
val description: String?, val description: String?,
val dependencies: ImmutableList<String>?,
val include: Boolean, val include: Boolean,
val compatiblePackages: ImmutableList<CompatiblePackage>?, val compatiblePackages: ImmutableList<CompatiblePackage>?,
val options: ImmutableList<Option>? val options: ImmutableList<Option>?
) { ) {
constructor(patch: PatchClass) : this( constructor(patch: Patch<*>) : this(
patch.patchName, patch.name.orEmpty(),
patch.description, patch.description,
patch.dependencies?.map { it.java.patchName }?.toImmutableList(), patch.use,
patch.include,
patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(), patch.compatiblePackages?.map { CompatiblePackage(it) }?.toImmutableList(),
patch.options?.map { Option(it) }?.toImmutableList()) patch.options.map { (_, option) -> Option(option) }.ifEmpty { null }?.toImmutableList()
)
fun compatibleWith(packageName: String) = compatiblePackages?.any { it.packageName == packageName } ?: true fun compatibleWith(packageName: String) =
compatiblePackages?.any { it.packageName == packageName } ?: true
fun supportsVersion(versionName: String) = fun supportsVersion(packageName: String, versionName: String): Boolean {
compatiblePackages?.any { compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == versionName } } } val packages = compatiblePackages ?: return true // Universal patch
?: true
return packages.any { pkg ->
if (pkg.packageName != packageName) {
return@any false
}
pkg.versions == null || pkg.versions.contains(versionName)
}
}
} }
@Immutable @Immutable
data class CompatiblePackage( data class CompatiblePackage(
val packageName: String, val packageName: String,
val versions: ImmutableList<String> val versions: ImmutableSet<String>?
) { ) {
constructor(pkg: Package) : this(pkg.name, pkg.versions.toList().toImmutableList()) constructor(pkg: Patch.CompatiblePackage) : this(
pkg.name,
pkg.versions?.toImmutableSet()
)
} }
@Immutable @Immutable
data class Option(val title: String, val key: String, val description: String, val required: Boolean, val type: Class<out PatchOption<*>>, val defaultValue: Any?) { data class Option(
constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required, option::class.java, option.value) val title: String,
val key: String,
val description: String,
val required: Boolean,
val type: Class<out PatchOption<*>>,
val defaultValue: Any?
) {
constructor(option: PatchOption<*>) : this(
option.title ?: option.key,
option.key,
option.description.orEmpty(),
option.required,
option::class.java,
option.value
)
} }

View file

@ -30,8 +30,6 @@ import app.revanced.manager.util.Options
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -172,22 +170,21 @@ class PatcherWorker(
args.options.forEach { (bundle, configuredPatchOptions) -> args.options.forEach { (bundle, configuredPatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach val patches = allPatches[bundle] ?: return@forEach
configuredPatchOptions.forEach { (patchName, options) -> configuredPatchOptions.forEach { (patchName, options) ->
patches.single { it.patchName == patchName }.options?.let { val patchOptions = patches.single { it.name == patchName }.options
options.forEach { (key, value) -> options.forEach { (key, value) ->
it[key] = value patchOptions[key] = value
}
} }
} }
} }
val patches = args.selectedPatches.flatMap { (bundle, selected) -> val patches = args.selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { selected.contains(it.patchName) } allPatches[bundle]?.filter { selected.contains(it.name) }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist") ?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
} }
// Ensure they are in the correct order so we can track progress properly. // Ensure they are in the correct order so we can track progress properly.
progressManager.replacePatchesList(patches.map { it.patchName }) progressManager.replacePatchesList(patches.map { it.name.orEmpty() })
updateProgress() // Loading patches updateProgress() // Loading patches
val inputFile = when (val selectedApp = args.input) { val inputFile = when (val selectedApp = args.input) {

View file

@ -29,7 +29,8 @@ import app.revanced.manager.R
import app.revanced.manager.data.platform.FileSystem import app.revanced.manager.data.platform.FileSystem
import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.Option
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.patcher.patch.PatchOption import app.revanced.patcher.patch.options.PatchOption
import app.revanced.patcher.patch.options.types.*
import org.koin.compose.rememberKoinInject import org.koin.compose.rememberKoinInject
// Composable functions do not support function references, so we have to use composable lambdas instead. // Composable functions do not support function references, so we have to use composable lambdas instead.
@ -195,8 +196,8 @@ fun OptionItem(option: Option, value: Any?, setValue: (Any?) -> Unit) {
val implementation = remember(option.type) { val implementation = remember(option.type) {
when (option.type) { when (option.type) {
// These are the only two types that are currently used by the official patches. // These are the only two types that are currently used by the official patches.
PatchOption.StringOption::class.java -> StringOption StringOption::class.java -> StringOption
PatchOption.BooleanOption::class.java -> BooleanOption BooleanOption::class.java -> BooleanOption
else -> UnknownOption else -> UnknownOption
} }
} }

View file

@ -31,6 +31,7 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.StandardCopyOption import java.nio.file.StandardCopyOption
import kotlin.io.path.deleteExisting import kotlin.io.path.deleteExisting
import kotlin.io.path.inputStream
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
class ImportExportViewModel( class ImportExportViewModel(
@ -59,11 +60,13 @@ class ImportExportViewModel(
} }
} }
knownPasswords.forEach { aliases.forEach { alias ->
if (tryKeystoreImport(KeystoreManager.DEFAULT, it, path)) { knownPasswords.forEach { pass ->
if (tryKeystoreImport(alias, pass, path)) {
return@launch return@launch
} }
} }
}
keystoreImportPath = path keystoreImportPath = path
} }
@ -77,10 +80,12 @@ class ImportExportViewModel(
tryKeystoreImport(cn, pass, keystoreImportPath!!) tryKeystoreImport(cn, pass, keystoreImportPath!!)
private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean { private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
if (keystoreManager.import(cn, pass, path)) { path.inputStream().use { stream ->
if (keystoreManager.import(cn, pass, stream)) {
cancelKeystoreImport() cancelKeystoreImport()
return true return true
} }
}
return false return false
} }
@ -174,6 +179,7 @@ class ImportExportViewModel(
} }
private companion object { private companion object {
val knownPasswords = setOf("ReVanced", "s3cur3p@ssw0rd") val knownPasswords = arrayOf("ReVanced", "s3cur3p@ssw0rd")
val aliases = arrayOf(KeystoreManager.DEFAULT, "alias", "ReVanced Key")
} }
} }

View file

@ -66,7 +66,11 @@ class PatchesSelectorViewModel(
bundle.patches.filter { it.compatibleWith(packageName) }.forEach { bundle.patches.filter { it.compatibleWith(packageName) }.forEach {
val targetList = when { val targetList = when {
it.compatiblePackages == null -> universal it.compatiblePackages == null -> universal
it.supportsVersion(input.selectedApp.version) -> supported it.supportsVersion(
input.selectedApp.packageName,
input.selectedApp.version
) -> supported
else -> unsupported else -> unsupported
} }
@ -254,17 +258,10 @@ class PatchesSelectorViewModel(
compatibleVersions.clear() compatibleVersions.clear()
} }
fun openUnsupportedDialog(unsupportedVersions: List<PatchInfo>) { fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
val set = HashSet<String>() compatibleVersions.addAll(unsupportedPatches.flatMap { patch ->
patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }?.versions.orEmpty()
unsupportedVersions.forEach { patch -> })
patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }
?.let { compatiblePackage ->
set.addAll(compatiblePackage.versions)
}
}
compatibleVersions.addAll(set)
} }
fun toggleFlag(flag: Int) { fun toggleFlag(flag: Int) {

View file

@ -54,8 +54,8 @@ class VersionSelectorViewModel(
bundle.patches.flatMap { patch -> bundle.patches.flatMap { patch ->
patch.compatiblePackages.orEmpty() patch.compatiblePackages.orEmpty()
.filter { it.packageName == packageName } .filter { it.packageName == packageName }
.onEach { if (it.versions.isEmpty()) patchesWithoutVersions++ } .onEach { if (it.versions == null) patchesWithoutVersions++ }
.flatMap { it.versions } .flatMap { it.versions.orEmpty() }
} }
}.groupingBy { it } }.groupingBy { it }
.eachCount() .eachCount()

View file

@ -1,101 +0,0 @@
package app.revanced.manager.util.signing
import android.util.Log
import app.revanced.manager.util.tag
import com.android.apksig.ApkSigner
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.ContentSigner
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.File
import java.io.InputStream
import java.math.BigInteger
import java.nio.file.Path
import java.security.*
import java.security.cert.X509Certificate
import java.util.*
import kotlin.io.path.exists
import kotlin.io.path.inputStream
import kotlin.io.path.name
import kotlin.io.path.outputStream
class Signer(
private val signingOptions: SigningOptions
) {
private val passwordCharArray = signingOptions.password.toCharArray()
private fun newKeystore(out: Path) {
val (publicKey, privateKey) = createKey()
val privateKS = KeyStore.getInstance("BKS", "BC")
privateKS.load(null, passwordCharArray)
privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
out.outputStream().use { stream -> privateKS.store(stream, passwordCharArray) }
}
fun regenerateKeystore() = newKeystore(signingOptions.keyStoreFilePath)
private fun createKey(): Pair<X509Certificate, PrivateKey> {
val gen = KeyPairGenerator.getInstance("RSA")
gen.initialize(4096)
val pair = gen.generateKeyPair()
var serialNumber: BigInteger
do serialNumber = BigInteger.valueOf(SecureRandom().nextLong()) while (serialNumber < BigInteger.ZERO)
val x500Name = X500Name("CN=${signingOptions.cn}")
val builder = X509v3CertificateBuilder(
x500Name,
serialNumber,
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
Locale.ENGLISH,
x500Name,
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
)
val signer: ContentSigner = JcaContentSignerBuilder("SHA256withRSA").build(pair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
}
private fun loadKeystore(): KeyStore {
val ks = signingOptions.keyStoreFilePath
if (!ks.exists()) newKeystore(ks) else {
Log.i(tag, "Found existing keystore: ${ks.name}")
}
Security.addProvider(BouncyCastleProvider())
val keyStore = KeyStore.getInstance("BKS", "BC")
ks.inputStream().use { keyStore.load(it, null) }
return keyStore
}
fun canUnlock(): Boolean {
val keyStore = loadKeystore()
val alias = keyStore.aliases().nextElement()
try {
keyStore.getKey(alias, passwordCharArray)
} catch (_: UnrecoverableKeyException) {
return false
}
return true
}
fun signApk(input: File, output: File) {
val keyStore = loadKeystore()
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)
signer.setInputApk(input)
signer.setOutputApk(output)
signer.build().sign()
}
}

View file

@ -1,9 +0,0 @@
package app.revanced.manager.util.signing
import java.nio.file.Path
data class SigningOptions(
val cn: String,
val password: String,
val keyStoreFilePath: Path
)

View file

@ -11,9 +11,8 @@ accompanist = "0.30.1"
serialization = "1.6.0" serialization = "1.6.0"
collection = "0.3.5" collection = "0.3.5"
room-version = "2.5.2" room-version = "2.5.2"
patcher = "14.2.1" revanced-patcher = "16.0.1"
apksign = "8.1.1" revanced-library = "1.1.1"
bcpkix-jdk18on = "1.76"
koin-version = "3.4.3" koin-version = "3.4.3"
koin-version-compose = "3.4.6" koin-version-compose = "3.4.6"
reimagined-navigation = "1.4.0" reimagined-navigation = "1.4.0"
@ -68,11 +67,8 @@ room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room-ver
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room-version" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room-version" }
# Patcher # Patcher
patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "patcher" } revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" }
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
# Signing
apksign = { group = "com.android.tools.build", name = "apksig", version.ref = "apksign" }
bcpkix-jdk18on = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bcpkix-jdk18on" }
# Koin # Koin
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" } koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" }

View file

@ -1,4 +1,5 @@
pluginManagement { pluginManagement {
repositories {
// TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed // TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) { val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
File(".gradle/gradle.properties").inputStream().use { File(".gradle/gradle.properties").inputStream().use {
@ -10,21 +11,25 @@ pluginManagement {
null to null null to null
} }
repositories { fun RepositoryHandler.githubPackages(name: String) = maven {
url = uri(name)
credentials {
username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
gradlePluginPortal() gradlePluginPortal()
google() google()
mavenCentral() mavenCentral()
maven("https://jitpack.io") maven("https://jitpack.io")
maven { githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
credentials {
username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
// TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed // TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) { val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
File(".gradle/gradle.properties").inputStream().use { File(".gradle/gradle.properties").inputStream().use {
@ -36,18 +41,19 @@ dependencyResolutionManagement {
null to null null to null
} }
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) fun RepositoryHandler.githubPackages(name: String) = maven {
repositories { url = uri(name)
google()
mavenCentral()
maven("https://jitpack.io")
maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials { credentials {
username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
} }
} }
google()
mavenCentral()
maven("https://jitpack.io")
githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
} }
} }
rootProject.name = "ReVanced Manager" rootProject.name = "ReVanced Manager"