Skip to content

Commit

Permalink
feat(installer): apk signing and installation
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 committed May 20, 2023
1 parent 762bfa8 commit 52ab793
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 16 deletions.
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
20 changes: 14 additions & 6 deletions app/src/main/java/app/revanced/manager/compose/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -16,4 +17,5 @@ val serviceModule = module {

single { provideReVancedService(get()) }
singleOf(::HttpService)
singleOf(::SignerService)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ val viewModelModule = module {
InstallerScreenViewModel(
input = it.get(),
selectedPatches = it.get(),
app = get()
app = get(),
signerService = get(),
)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -125,7 +126,7 @@ fun AppSelectorScreen(

ListItem(
modifier = Modifier.clickable {
pickApkLauncher.launch("*/*")
pickApkLauncher.launch(APK_MIMETYPE)
},
leadingContent = {
Box(Modifier.size(36.dp), Alignment.Center) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 ->
Expand All @@ -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))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,55 @@ 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
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
Expand All @@ -51,7 +67,7 @@ class InstallerScreenViewModel(
outputFile.path,
selectedPatches,
input.packageName,
input.packageName,
input.version,
)
)
)
Expand All @@ -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 }
}
Expand Down Expand Up @@ -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() {
Expand All @@ -105,5 +151,8 @@ class InstallerScreenViewModel(
app.unregisterReceiver(installBroadcastReceiver)
workManager.cancelWorkById(patcherWorker.id)
// logs.clear()

outputFile.delete()
signedFile.delete()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package app.revanced.manager.compose.util.signing

data class SigningOptions(
val cn: String,
val password: String,
val keyStoreFilePath: String
)
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 52ab793

Please sign in to comment.