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)
// ReVanced
implementation(libs.patcher)
// Signing
implementation(libs.apksign)
implementation(libs.bcpkix.jdk18on)
implementation(libs.revanced.patcher)
implementation(libs.revanced.library)
implementation(libs.libsu.core)
implementation(libs.libsu.service)

View File

@ -2,19 +2,19 @@ package app.revanced.manager.domain.manager
import android.app.Application
import android.content.Context
import app.revanced.manager.util.signing.Signer
import app.revanced.manager.util.signing.SigningOptions
import app.revanced.library.ApkSigner
import app.revanced.library.ApkUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import kotlin.io.path.exists
import java.security.UnrecoverableKeyException
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
companion object {
companion object Constants {
/**
* Default alias and password for the keystore.
*/
@ -22,37 +22,55 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
}
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 {
prefs.keystoreCommonName.value = cn
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) {
Signer(
SigningOptions(
prefs.keystoreCommonName.get(),
prefs.keystorePass.get(),
keystorePath
)
).signApk(
input,
output
)
ApkUtils.sign(input, output, signingOptions())
}
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)
}
suspend fun import(cn: String, pass: String, keystore: Path): Boolean {
if (!Signer(SigningOptions(cn, pass, keystore)).canUnlock()) {
suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
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
}
withContext(Dispatchers.IO) {
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)
Files.write(keystorePath.toPath(), keystoreData)
}
updatePrefs(cn, pass)
@ -63,7 +81,7 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
suspend fun export(target: OutputStream) {
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
import app.revanced.library.ApkUtils
import app.revanced.manager.ui.viewmodel.ManagerLogger
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.patch.PatchClass
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -13,7 +14,7 @@ import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.logging.Logger
internal typealias PatchList = List<PatchClass>
internal typealias PatchList = List<Patch<*>>
class Session(
cacheDir: String,
@ -69,7 +70,8 @@ class Session(
logger.info("Writing patched files...")
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")
@ -85,7 +87,7 @@ class Session(
}
companion object {
operator fun PatchResult.component1() = patchName
operator fun PatchResult.component1() = patch.name
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 app.revanced.manager.util.tag
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.patch.PatchClass
import app.revanced.patcher.patch.Patch
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(
object : Iterable<PatchClass> {
private fun load(): List<PatchClass> = PatchBundleLoader.Dex(bundleJar)
object : Iterable<Patch<*>> {
private fun load(): Iterable<Patch<*>> = PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null)
override fun iterator() = load().iterator()
override fun iterator(): Iterator<Patch<*>> = load().iterator()
},
integrations
) {

View File

@ -1,50 +1,70 @@
package app.revanced.manager.patcher.patch
import androidx.compose.runtime.Immutable
import app.revanced.patcher.annotation.Package
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
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 app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.options.PatchOption
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
data class PatchInfo(
val name: String,
val description: String?,
val dependencies: ImmutableList<String>?,
val include: Boolean,
val compatiblePackages: ImmutableList<CompatiblePackage>?,
val options: ImmutableList<Option>?
) {
constructor(patch: PatchClass) : this(
patch.patchName,
constructor(patch: Patch<*>) : this(
patch.name.orEmpty(),
patch.description,
patch.dependencies?.map { it.java.patchName }?.toImmutableList(),
patch.include,
patch.use,
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) =
compatiblePackages?.any { compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == versionName } } }
?: true
fun supportsVersion(packageName: String, versionName: String): Boolean {
val packages = compatiblePackages ?: return true // Universal patch
return packages.any { pkg ->
if (pkg.packageName != packageName) {
return@any false
}
pkg.versions == null || pkg.versions.contains(versionName)
}
}
}
@Immutable
data class CompatiblePackage(
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
data class Option(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.description, option.required, option::class.java, option.value)
data class Option(
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.PatchesSelection
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.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
@ -172,22 +170,21 @@ class PatcherWorker(
args.options.forEach { (bundle, configuredPatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach
configuredPatchOptions.forEach { (patchName, options) ->
patches.single { it.patchName == patchName }.options?.let {
options.forEach { (key, value) ->
it[key] = value
}
val patchOptions = patches.single { it.name == patchName }.options
options.forEach { (key, value) ->
patchOptions[key] = value
}
}
}
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")
}
// 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
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.patcher.patch.Option
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
// 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) {
when (option.type) {
// These are the only two types that are currently used by the official patches.
PatchOption.StringOption::class.java -> StringOption
PatchOption.BooleanOption::class.java -> BooleanOption
StringOption::class.java -> StringOption
BooleanOption::class.java -> BooleanOption
else -> UnknownOption
}
}

View File

@ -31,6 +31,7 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import kotlin.io.path.deleteExisting
import kotlin.io.path.inputStream
@OptIn(ExperimentalSerializationApi::class)
class ImportExportViewModel(
@ -59,9 +60,11 @@ class ImportExportViewModel(
}
}
knownPasswords.forEach {
if (tryKeystoreImport(KeystoreManager.DEFAULT, it, path)) {
return@launch
aliases.forEach { alias ->
knownPasswords.forEach { pass ->
if (tryKeystoreImport(alias, pass, path)) {
return@launch
}
}
}
@ -77,9 +80,11 @@ class ImportExportViewModel(
tryKeystoreImport(cn, pass, keystoreImportPath!!)
private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
if (keystoreManager.import(cn, pass, path)) {
cancelKeystoreImport()
return true
path.inputStream().use { stream ->
if (keystoreManager.import(cn, pass, stream)) {
cancelKeystoreImport()
return true
}
}
return false
@ -174,6 +179,7 @@ class ImportExportViewModel(
}
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 {
val targetList = when {
it.compatiblePackages == null -> universal
it.supportsVersion(input.selectedApp.version) -> supported
it.supportsVersion(
input.selectedApp.packageName,
input.selectedApp.version
) -> supported
else -> unsupported
}
@ -254,17 +258,10 @@ class PatchesSelectorViewModel(
compatibleVersions.clear()
}
fun openUnsupportedDialog(unsupportedVersions: List<PatchInfo>) {
val set = HashSet<String>()
unsupportedVersions.forEach { patch ->
patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }
?.let { compatiblePackage ->
set.addAll(compatiblePackage.versions)
}
}
compatibleVersions.addAll(set)
fun openUnsupportedDialog(unsupportedPatches: List<PatchInfo>) {
compatibleVersions.addAll(unsupportedPatches.flatMap { patch ->
patch.compatiblePackages?.find { it.packageName == input.selectedApp.packageName }?.versions.orEmpty()
})
}
fun toggleFlag(flag: Int) {

View File

@ -54,8 +54,8 @@ class VersionSelectorViewModel(
bundle.patches.flatMap { patch ->
patch.compatiblePackages.orEmpty()
.filter { it.packageName == packageName }
.onEach { if (it.versions.isEmpty()) patchesWithoutVersions++ }
.flatMap { it.versions }
.onEach { if (it.versions == null) patchesWithoutVersions++ }
.flatMap { it.versions.orEmpty() }
}
}.groupingBy { it }
.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"
collection = "0.3.5"
room-version = "2.5.2"
patcher = "14.2.1"
apksign = "8.1.1"
bcpkix-jdk18on = "1.76"
revanced-patcher = "16.0.1"
revanced-library = "1.1.1"
koin-version = "3.4.3"
koin-version-compose = "3.4.6"
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" }
# Patcher
patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "patcher" }
# Signing
apksign = { group = "com.android.tools.build", name = "apksig", version.ref = "apksign" }
bcpkix-jdk18on = { group = "org.bouncycastle", name = "bcpkix-jdk18on", version.ref = "bcpkix-jdk18on" }
revanced-patcher = { group = "app.revanced", name = "revanced-patcher", version.ref = "revanced-patcher" }
revanced-library = { group = "app.revanced", name = "revanced-library", version.ref = "revanced-library" }
# Koin
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin-version" }

View File

@ -1,53 +1,59 @@
pluginManagement {
// TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
File(".gradle/gradle.properties").inputStream().use {
java.util.Properties().apply { load(it) }.let {
it.getProperty("gpr.user") to it.getProperty("gpr.key")
repositories {
// TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
File(".gradle/gradle.properties").inputStream().use {
java.util.Properties().apply { load(it) }.let {
it.getProperty("gpr.user") to it.getProperty("gpr.key")
}
}
} else {
null to null
}
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")
}
}
} else {
null to null
}
repositories {
gradlePluginPortal()
google()
mavenCentral()
maven("https://jitpack.io")
maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = gprUser ?: providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
password = gprKey ?: providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
}
}
githubPackages("https://maven.pkg.github.com/revanced/revanced-patcher")
githubPackages("https://maven.pkg.github.com/revanced/revanced-library")
}
}
dependencyResolutionManagement {
// TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
File(".gradle/gradle.properties").inputStream().use {
java.util.Properties().apply { load(it) }.let {
it.getProperty("gpr.user") to it.getProperty("gpr.key")
}
}
} else {
null to null
}
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
// TODO: remove this once https://github.com/gradle/gradle/issues/23572 is fixed
val (gprUser, gprKey) = if (File(".gradle/gradle.properties").exists()) {
File(".gradle/gradle.properties").inputStream().use {
java.util.Properties().apply { load(it) }.let {
it.getProperty("gpr.user") to it.getProperty("gpr.key")
}
}
} else {
null to null
}
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")
}
}
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"