mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat(installer): apk signing and installation
This commit is contained in:
parent
762bfa8514
commit
52ab7937bd
@ -81,6 +81,10 @@ dependencies {
|
||||
// ReVanced
|
||||
implementation("app.revanced:revanced-patcher:7.1.0")
|
||||
|
||||
// Signing
|
||||
implementation("com.android.tools.build:apksig:8.1.0-beta02")
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
|
||||
// Koin
|
||||
val koinVersion = "3.4.0"
|
||||
implementation("io.insert-koin:koin-android:$koinVersion")
|
||||
|
@ -75,12 +75,20 @@ class MainActivity : ComponentActivity() {
|
||||
vm = getViewModel { parametersOf(destination.input) }
|
||||
)
|
||||
|
||||
is Destination.Installer -> InstallerScreen(getViewModel {
|
||||
parametersOf(
|
||||
destination.input,
|
||||
destination.selectedPatches
|
||||
)
|
||||
})
|
||||
is Destination.Installer -> InstallerScreen(
|
||||
onBackClick = {
|
||||
with(navController) {
|
||||
popAll()
|
||||
navigate(Destination.Dashboard)
|
||||
}
|
||||
},
|
||||
vm = getViewModel {
|
||||
parametersOf(
|
||||
destination.input,
|
||||
destination.selectedPatches
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package app.revanced.manager.compose.di
|
||||
|
||||
import app.revanced.manager.compose.network.service.HttpService
|
||||
import app.revanced.manager.compose.network.service.ReVancedService
|
||||
import app.revanced.manager.compose.patcher.SignerService
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
@ -16,4 +17,5 @@ val serviceModule = module {
|
||||
|
||||
single { provideReVancedService(get()) }
|
||||
singleOf(::HttpService)
|
||||
singleOf(::SignerService)
|
||||
}
|
@ -21,7 +21,8 @@ val viewModelModule = module {
|
||||
InstallerScreenViewModel(
|
||||
input = it.get(),
|
||||
selectedPatches = it.get(),
|
||||
app = get()
|
||||
app = get(),
|
||||
signerService = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
package app.revanced.manager.compose.patcher
|
||||
|
||||
import android.app.Application
|
||||
import app.revanced.manager.compose.util.signing.Signer
|
||||
import app.revanced.manager.compose.util.signing.SigningOptions
|
||||
|
||||
class SignerService(app: Application) {
|
||||
private val options = SigningOptions("ReVanced", "ReVanced", app.dataDir.resolve("manager.keystore").path)
|
||||
|
||||
fun createSigner() = Signer(options)
|
||||
}
|
@ -7,7 +7,6 @@ import androidx.work.workDataOf
|
||||
import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.patcher.Session
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
|
@ -26,6 +26,7 @@ import app.revanced.manager.compose.ui.component.AppIcon
|
||||
import app.revanced.manager.compose.ui.component.AppTopBar
|
||||
import app.revanced.manager.compose.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.compose.ui.viewmodel.AppSelectorViewModel
|
||||
import app.revanced.manager.compose.util.APK_MIMETYPE
|
||||
import app.revanced.manager.compose.util.PM
|
||||
import app.revanced.manager.compose.util.PackageInfo
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
@ -125,7 +126,7 @@ fun AppSelectorScreen(
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
pickApkLauncher.launch("*/*")
|
||||
pickApkLauncher.launch(APK_MIMETYPE)
|
||||
},
|
||||
leadingContent = {
|
||||
Box(Modifier.size(36.dp), Alignment.Center) {
|
||||
|
@ -1,8 +1,11 @@
|
||||
package app.revanced.manager.compose.ui.screen
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
@ -13,17 +16,21 @@ import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.ui.component.AppScaffold
|
||||
import app.revanced.manager.compose.ui.component.AppTopBar
|
||||
import app.revanced.manager.compose.ui.viewmodel.InstallerScreenViewModel
|
||||
import app.revanced.manager.compose.util.APK_MIMETYPE
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun InstallerScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: InstallerScreenViewModel
|
||||
) {
|
||||
val exportApkLauncher = rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
|
||||
|
||||
AppScaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(R.string.installer),
|
||||
onBackClick = { },
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
@ -47,6 +54,20 @@ fun InstallerScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = vm::installApk,
|
||||
enabled = vm.canInstall
|
||||
) {
|
||||
Text(stringResource(R.string.install_app))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { exportApkLauncher.launch("${vm.packageName}.apk") },
|
||||
enabled = vm.canInstall
|
||||
) {
|
||||
Text(stringResource(R.string.export_app))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,12 +6,16 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.work.*
|
||||
import app.revanced.manager.compose.R
|
||||
import app.revanced.manager.compose.patcher.SignerService
|
||||
import app.revanced.manager.compose.patcher.worker.PatcherProgressManager
|
||||
import app.revanced.manager.compose.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.compose.patcher.worker.StepGroup
|
||||
@ -19,26 +23,38 @@ import app.revanced.manager.compose.service.InstallService
|
||||
import app.revanced.manager.compose.service.UninstallService
|
||||
import app.revanced.manager.compose.util.PM
|
||||
import app.revanced.manager.compose.util.PackageInfo
|
||||
import app.revanced.manager.compose.util.toast
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
class InstallerScreenViewModel(
|
||||
input: PackageInfo,
|
||||
selectedPatches: List<String>,
|
||||
private val app: Application
|
||||
private val app: Application,
|
||||
private val signerService: SignerService
|
||||
) : ViewModel() {
|
||||
var stepGroups by mutableStateOf<List<StepGroup>>(PatcherProgressManager.generateGroupsList(app, selectedPatches))
|
||||
private set
|
||||
|
||||
val packageName = input.packageName
|
||||
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
// TODO: handle app installation as a step.
|
||||
// TODO: get rid of these and use stepGroups instead.
|
||||
var installStatus by mutableStateOf<Boolean?>(null)
|
||||
var pmStatus by mutableStateOf(-999)
|
||||
var extra by mutableStateOf("")
|
||||
|
||||
private val outputFile = File(app.cacheDir, "output.apk")
|
||||
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
|
||||
private var hasSigned = false
|
||||
private var patcherStatus by mutableStateOf<Boolean?>(null)
|
||||
private var isInstalling by mutableStateOf(false)
|
||||
|
||||
val canInstall by derivedStateOf { patcherStatus == true && !isInstalling }
|
||||
|
||||
|
||||
private val patcherWorker =
|
||||
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
|
||||
@ -51,7 +67,7 @@ class InstallerScreenViewModel(
|
||||
outputFile.path,
|
||||
selectedPatches,
|
||||
input.packageName,
|
||||
input.packageName,
|
||||
input.version,
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -62,7 +78,10 @@ class InstallerScreenViewModel(
|
||||
private val observer = Observer { workInfo: WorkInfo -> // observer for observing patch status
|
||||
when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> workInfo.progress
|
||||
WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData
|
||||
WorkInfo.State.FAILED, WorkInfo.State.SUCCEEDED -> workInfo.outputData.also {
|
||||
patcherStatus = workInfo.state == WorkInfo.State.SUCCEEDED
|
||||
}
|
||||
|
||||
else -> null
|
||||
}?.let { PatcherProgressManager.groupsFromWorkData(it) }?.let { stepGroups = it }
|
||||
}
|
||||
@ -91,8 +110,35 @@ class InstallerScreenViewModel(
|
||||
})
|
||||
}
|
||||
|
||||
fun installApk(apk: List<File>) {
|
||||
PM.installApp(apk, app)
|
||||
private fun signApk(): Boolean {
|
||||
if (!hasSigned) {
|
||||
try {
|
||||
signerService.createSigner().signApk(outputFile, signedFile)
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
app.toast(app.getString(R.string.sign_fail, e::class.simpleName))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun export(uri: Uri?) = uri?.let {
|
||||
if (signApk()) {
|
||||
Files.copy(signedFile.toPath(), app.contentResolver.openOutputStream(it))
|
||||
app.toast(app.getString(R.string.export_app_success))
|
||||
}
|
||||
}
|
||||
|
||||
fun installApk() {
|
||||
isInstalling = true
|
||||
try {
|
||||
if (!signApk()) return
|
||||
PM.installApp(listOf(signedFile), app)
|
||||
} finally {
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
|
||||
fun postInstallStatus() {
|
||||
@ -105,5 +151,8 @@ class InstallerScreenViewModel(
|
||||
app.unregisterReceiver(installBroadcastReceiver)
|
||||
workManager.cancelWorkById(patcherWorker.id)
|
||||
// logs.clear()
|
||||
|
||||
outputFile.delete()
|
||||
signedFile.delete()
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
|
||||
const val APK_MIMETYPE = "application/vnd.android.package-archive"
|
||||
|
||||
fun Context.openUrl(url: String) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
|
||||
|
@ -0,0 +1,77 @@
|
||||
package app.revanced.manager.compose.util.signing
|
||||
|
||||
import android.util.Log
|
||||
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.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
|
||||
class Signer(
|
||||
private val signingOptions: SigningOptions
|
||||
) {
|
||||
private val passwordCharArray = signingOptions.password.toCharArray()
|
||||
private fun newKeystore(out: File) {
|
||||
val (publicKey, privateKey) = createKey()
|
||||
val privateKS = KeyStore.getInstance("BKS", "BC")
|
||||
privateKS.load(null, passwordCharArray)
|
||||
privateKS.setKeyEntry("alias", privateKey, passwordCharArray, arrayOf(publicKey))
|
||||
privateKS.store(FileOutputStream(out), passwordCharArray)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fun signApk(input: File, output: File) {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
val ks = File(signingOptions.keyStoreFilePath)
|
||||
if (!ks.exists()) newKeystore(ks) else {
|
||||
Log.i("revanced-manager", "Found existing keystore: ${ks.name}")
|
||||
}
|
||||
|
||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
|
||||
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()
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package app.revanced.manager.compose.util.signing
|
||||
|
||||
data class SigningOptions(
|
||||
val cn: String,
|
||||
val password: String,
|
||||
val keyStoreFilePath: String
|
||||
)
|
@ -62,6 +62,10 @@
|
||||
<string name="app_not_supported">Some of the patches do not support this app version (%s). The patches only support the following versions: %s.</string>
|
||||
|
||||
<string name="installer">Installer</string>
|
||||
<string name="install_app">Install</string>
|
||||
<string name="export_app">Export</string>
|
||||
<string name="export_app_success">Apk exported</string>
|
||||
<string name="sign_fail">Failed to sign Apk: %s</string>
|
||||
|
||||
<string name="patcher_step_group_prepare">Preparation</string>
|
||||
<string name="patcher_step_unpack">Unpack Apk</string>
|
||||
|
Loading…
Reference in New Issue
Block a user