feat(installer): apk signing and installation

This commit is contained in:
Ax333l 2023-05-20 12:14:30 +02:00
parent 8c58c41bb7
commit 2012258fe7
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
13 changed files with 201 additions and 16 deletions

View File

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

View File

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

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

View File

@ -21,7 +21,8 @@ val viewModelModule = module {
InstallerScreenViewModel(
input = 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.patcher.Session
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
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.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) {

View File

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

View File

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

View File

@ -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 {

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="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>