feat(installer): apk signing and installation

This commit is contained in:
Ax333l 2023-05-20 12:14:30 +02:00
parent 762bfa8514
commit 52ab7937bd
13 changed files with 201 additions and 16 deletions

View File

@ -81,6 +81,10 @@ dependencies {
// ReVanced // ReVanced
implementation("app.revanced:revanced-patcher:7.1.0") 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 // Koin
val koinVersion = "3.4.0" val koinVersion = "3.4.0"
implementation("io.insert-koin:koin-android:$koinVersion") implementation("io.insert-koin:koin-android:$koinVersion")

View File

@ -75,12 +75,20 @@ class MainActivity : ComponentActivity() {
vm = getViewModel { parametersOf(destination.input) } vm = getViewModel { parametersOf(destination.input) }
) )
is Destination.Installer -> InstallerScreen(getViewModel { is Destination.Installer -> InstallerScreen(
parametersOf( onBackClick = {
destination.input, with(navController) {
destination.selectedPatches popAll()
) navigate(Destination.Dashboard)
}) }
},
vm = getViewModel {
parametersOf(
destination.input,
destination.selectedPatches
)
}
)
} }
} }
} }

View File

@ -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.HttpService
import app.revanced.manager.compose.network.service.ReVancedService 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.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
@ -16,4 +17,5 @@ val serviceModule = module {
single { provideReVancedService(get()) } single { provideReVancedService(get()) }
singleOf(::HttpService) singleOf(::HttpService)
singleOf(::SignerService)
} }

View File

@ -21,7 +21,8 @@ val viewModelModule = module {
InstallerScreenViewModel( InstallerScreenViewModel(
input = it.get(), input = it.get(),
selectedPatches = it.get(), selectedPatches = it.get(),
app = get() app = get(),
signerService = get(),
) )
} }
} }

View File

@ -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)
}

View File

@ -7,7 +7,6 @@ import androidx.work.workDataOf
import app.revanced.manager.compose.R import app.revanced.manager.compose.R
import app.revanced.manager.compose.patcher.Session import app.revanced.manager.compose.patcher.Session
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View File

@ -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.AppTopBar
import app.revanced.manager.compose.ui.component.LoadingIndicator import app.revanced.manager.compose.ui.component.LoadingIndicator
import app.revanced.manager.compose.ui.viewmodel.AppSelectorViewModel 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.PM
import app.revanced.manager.compose.util.PackageInfo import app.revanced.manager.compose.util.PackageInfo
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@ -125,7 +126,7 @@ fun AppSelectorScreen(
ListItem( ListItem(
modifier = Modifier.clickable { modifier = Modifier.clickable {
pickApkLauncher.launch("*/*") pickApkLauncher.launch(APK_MIMETYPE)
}, },
leadingContent = { leadingContent = {
Box(Modifier.size(36.dp), Alignment.Center) { Box(Modifier.size(36.dp), Alignment.Center) {

View File

@ -1,8 +1,11 @@
package app.revanced.manager.compose.ui.screen 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.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text 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.AppScaffold
import app.revanced.manager.compose.ui.component.AppTopBar import app.revanced.manager.compose.ui.component.AppTopBar
import app.revanced.manager.compose.ui.viewmodel.InstallerScreenViewModel import app.revanced.manager.compose.ui.viewmodel.InstallerScreenViewModel
import app.revanced.manager.compose.util.APK_MIMETYPE
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun InstallerScreen( fun InstallerScreen(
onBackClick: () -> Unit,
vm: InstallerScreenViewModel vm: InstallerScreenViewModel
) { ) {
val exportApkLauncher = rememberLauncherForActivityResult(CreateDocument(APK_MIMETYPE), vm::export)
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(
title = stringResource(R.string.installer), title = stringResource(R.string.installer),
onBackClick = { }, onBackClick = onBackClick,
) )
} }
) { paddingValues -> ) { 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))
}
} }
} }
} }

View File

@ -6,12 +6,16 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller
import android.net.Uri
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.work.* 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.PatcherProgressManager
import app.revanced.manager.compose.patcher.worker.PatcherWorker import app.revanced.manager.compose.patcher.worker.PatcherWorker
import app.revanced.manager.compose.patcher.worker.StepGroup 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.service.UninstallService
import app.revanced.manager.compose.util.PM import app.revanced.manager.compose.util.PM
import app.revanced.manager.compose.util.PackageInfo import app.revanced.manager.compose.util.PackageInfo
import app.revanced.manager.compose.util.toast
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.io.File import java.io.File
import java.nio.file.Files
class InstallerScreenViewModel( class InstallerScreenViewModel(
input: PackageInfo, input: PackageInfo,
selectedPatches: List<String>, selectedPatches: List<String>,
private val app: Application private val app: Application,
private val signerService: SignerService
) : ViewModel() { ) : ViewModel() {
var stepGroups by mutableStateOf<List<StepGroup>>(PatcherProgressManager.generateGroupsList(app, selectedPatches)) var stepGroups by mutableStateOf<List<StepGroup>>(PatcherProgressManager.generateGroupsList(app, selectedPatches))
private set private set
val packageName = input.packageName
private val workManager = WorkManager.getInstance(app) 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 installStatus by mutableStateOf<Boolean?>(null)
var pmStatus by mutableStateOf(-999) var pmStatus by mutableStateOf(-999)
var extra by mutableStateOf("") var extra by mutableStateOf("")
private val outputFile = File(app.cacheDir, "output.apk") 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 = private val patcherWorker =
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
@ -51,7 +67,7 @@ class InstallerScreenViewModel(
outputFile.path, outputFile.path,
selectedPatches, selectedPatches,
input.packageName, input.packageName,
input.packageName, input.version,
) )
) )
) )
@ -62,7 +78,10 @@ class InstallerScreenViewModel(
private val observer = Observer { workInfo: WorkInfo -> // observer for observing patch status private val observer = Observer { workInfo: WorkInfo -> // observer for observing patch status
when (workInfo.state) { when (workInfo.state) {
WorkInfo.State.RUNNING -> workInfo.progress 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 else -> null
}?.let { PatcherProgressManager.groupsFromWorkData(it) }?.let { stepGroups = it } }?.let { PatcherProgressManager.groupsFromWorkData(it) }?.let { stepGroups = it }
} }
@ -91,8 +110,35 @@ class InstallerScreenViewModel(
}) })
} }
fun installApk(apk: List<File>) { private fun signApk(): Boolean {
PM.installApp(apk, app) 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() { fun postInstallStatus() {
@ -105,5 +151,8 @@ class InstallerScreenViewModel(
app.unregisterReceiver(installBroadcastReceiver) app.unregisterReceiver(installBroadcastReceiver)
workManager.cancelWorkById(patcherWorker.id) workManager.cancelWorkById(patcherWorker.id)
// logs.clear() // logs.clear()
outputFile.delete()
signedFile.delete()
} }
} }

View File

@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable
import android.widget.Toast import android.widget.Toast
import androidx.core.net.toUri import androidx.core.net.toUri
const val APK_MIMETYPE = "application/vnd.android.package-archive"
fun Context.openUrl(url: String) { fun Context.openUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply { startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {

View File

@ -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()
}
}

View File

@ -0,0 +1,7 @@
package app.revanced.manager.compose.util.signing
data class SigningOptions(
val cn: String,
val password: String,
val keyStoreFilePath: String
)

View File

@ -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="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="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_group_prepare">Preparation</string>
<string name="patcher_step_unpack">Unpack Apk</string> <string name="patcher_step_unpack">Unpack Apk</string>