mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 09:07:47 +01:00
chore: switch to revanced library and bump patcher (#1314)
This commit is contained in:
parent
f78b56ef0a
commit
e232044157
20 changed files with 171 additions and 778 deletions
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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())
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue