Skip to content

Commit

Permalink
feat: keystore import/export (ReVanced#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 authored Jun 11, 2023
1 parent 971277e commit 919b6b7
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 39 deletions.
4 changes: 2 additions & 2 deletions app/src/main/java/app/revanced/manager/di/ManagerModule.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package app.revanced.manager.di

import app.revanced.manager.patcher.SignerService
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.util.PM
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module

val managerModule = module {
singleOf(::SignerService)
singleOf(::KeystoreManager)
singleOf(::PM)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ val viewModelModule = module {
viewModelOf(::SourcesViewModel)
viewModelOf(::InstallerViewModel)
viewModelOf(::UpdateSettingsViewModel)
viewModelOf(::ImportExportViewModel)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package app.revanced.manager.domain.manager

import android.app.Application
import app.revanced.manager.util.signing.Signer
import app.revanced.manager.util.signing.SigningOptions
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import kotlin.io.path.exists

class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
companion object {
/**
* Default common name and password for the keystore.
*/
const val DEFAULT = "ReVanced"

/**
* The default password used by the Flutter version.
*/
const val FLUTTER_MANAGER_PASSWORD = "s3cur3p@ssw0rd"
}

private val keystorePath = app.dataDir.resolve("manager.keystore").toPath()
private fun options(
cn: String = prefs.keystoreCommonName!!,
pass: String = prefs.keystorePass!!
) = SigningOptions(cn, pass, keystorePath)

private fun updatePrefs(cn: String, pass: String) {
prefs.keystoreCommonName = cn
prefs.keystorePass = pass
}

fun sign(input: File, output: File) = Signer(options()).signApk(input, output)

init {
if (!keystorePath.exists()) {
regenerate()
}
}

fun regenerate() = Signer(options(DEFAULT, DEFAULT)).regenerateKeystore().also {
updatePrefs(DEFAULT, DEFAULT)
}

fun import(cn: String, pass: String, keystore: InputStream) {
// TODO: check if the user actually provided the correct password
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)

updatePrefs(cn, pass)
}

fun export(target: OutputStream) {
Files.copy(keystorePath, target)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ class PreferencesManager(
var dynamicColor by booleanPreference("dynamic_color", true)
var theme by enumPreference("theme", Theme.SYSTEM)
//var sentry by booleanPreference("sentry", true)

var keystoreCommonName by stringPreference("keystore_cn", KeystoreManager.DEFAULT)
var keystorePass by stringPreference("keystore_pass", KeystoreManager.DEFAULT)
}
11 changes: 0 additions & 11 deletions app/src/main/java/app/revanced/manager/patcher/SignerService.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,27 +1,56 @@
package app.revanced.manager.ui.screen.settings

import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
import app.revanced.manager.domain.manager.KeystoreManager.Companion.DEFAULT
import app.revanced.manager.domain.manager.KeystoreManager.Companion.FLUTTER_MANAGER_PASSWORD
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.FileSelector
import app.revanced.manager.ui.component.GroupHeader
import org.koin.androidx.compose.getViewModel
import org.koin.compose.rememberKoinInject

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImportExportSettingsScreen(
onBackClick: () -> Unit
onBackClick: () -> Unit,
vm: ImportExportViewModel = getViewModel()
) {
var showImportKeystoreDialog by rememberSaveable { mutableStateOf(false) }
var showExportKeystoreDialog by rememberSaveable { mutableStateOf(false) }

if (showImportKeystoreDialog) {
ImportKeystoreDialog(
onDismissRequest = { showImportKeystoreDialog = false },
onImport = vm::import
)
}
if (showExportKeystoreDialog) {
ExportKeystoreDialog(
onDismissRequest = { showExportKeystoreDialog = false },
onExport = vm::export
)
}

Scaffold(
topBar = {
AppTopBar(
Expand All @@ -37,16 +66,123 @@ fun ImportExportSettingsScreen(
.verticalScroll(rememberScrollState())
) {
GroupHeader(stringResource(R.string.signing))
ListItem(
modifier = Modifier.clickable { },
headlineContent = { Text(stringResource(R.string.import_keystore)) },
supportingContent = { Text(stringResource(R.string.import_keystore_descripion)) }
GroupItem(
onClick = {
showImportKeystoreDialog = true
},
headline = R.string.import_keystore,
description = R.string.import_keystore_descripion
)
GroupItem(
onClick = {
showExportKeystoreDialog = true
},
headline = R.string.export_keystore,
description = R.string.export_keystore_description
)
ListItem(
modifier = Modifier.clickable { },
headlineContent = { Text(stringResource(R.string.export_keystore)) },
supportingContent = { Text(stringResource(R.string.export_keystore_description)) }
GroupItem(
onClick = vm::regenerate,
headline = R.string.regenerate_keystore,
description = R.string.regenerate_keystore_description
)
}
}
}

@Composable
private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes description: Int) =
ListItem(
modifier = Modifier.clickable { onClick() },
headlineContent = { Text(stringResource(headline)) },
supportingContent = { Text(stringResource(description)) }
)

@Composable
fun ExportKeystoreDialog(
onDismissRequest: () -> Unit,
onExport: (Uri) -> Unit
) {
val activityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { uri ->
uri?.let {
onExport(it)
onDismissRequest()
}
}
val prefs: PreferencesManager = rememberKoinInject()

AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
Button(
onClick = { activityLauncher.launch("Manager.keystore") }
) {
Text(stringResource(R.string.select_file))
}
},
title = { Text(stringResource(R.string.export_keystore)) },
text = {
Column {
Text("Current common name: ${prefs.keystoreCommonName}")
Text("Current password: ${prefs.keystorePass}")
}
}
)
}

@Composable
fun ImportKeystoreDialog(
onDismissRequest: () -> Unit, onImport: (Uri, String, String) -> Unit
) {
var cn by rememberSaveable { mutableStateOf(DEFAULT) }
var pass by rememberSaveable { mutableStateOf(DEFAULT) }

AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
FileSelector(
mime = "*/*",
onSelect = {
onImport(it, cn, pass)
onDismissRequest()
}
) {
Text(stringResource(R.string.select_file))
}
},
title = { Text(stringResource(R.string.import_keystore)) },
text = {
Column {
TextField(
value = cn,
onValueChange = { cn = it },
label = { Text("Common Name") }
)
TextField(
value = pass,
onValueChange = { pass = it },
label = { Text("Password") }
)

Text("Credential presets")

Button(
onClick = {
cn = DEFAULT
pass = DEFAULT
}
) {
Text(stringResource(R.string.import_keystore_preset_default))
}
Button(
onClick = {
cn = DEFAULT
pass = FLUTTER_MANAGER_PASSWORD
}
) {
Text(stringResource(R.string.import_keystore_preset_flutter))
}
}
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package app.revanced.manager.ui.viewmodel


import android.app.Application
import android.net.Uri
import androidx.lifecycle.ViewModel
import app.revanced.manager.R
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.util.toast

class ImportExportViewModel(private val app: Application, private val keystoreManager: KeystoreManager) : ViewModel() {
private val contentResolver = app.contentResolver

fun import(content: Uri, cn: String, pass: String) =
keystoreManager.import(cn, pass, contentResolver.openInputStream(content)!!)

fun export(target: Uri) = keystoreManager.export(contentResolver.openOutputStream(target)!!)

fun regenerate() = keystoreManager.regenerate().also {
app.toast(app.getString(R.string.regenerate_keystore_success))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
Expand All @@ -15,8 +16,8 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
import androidx.work.*
import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.R
import app.revanced.manager.patcher.SignerService
import app.revanced.manager.patcher.worker.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.patcher.worker.StepGroup
Expand All @@ -25,6 +26,7 @@ import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.AppInfo
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
Expand All @@ -38,7 +40,7 @@ class InstallerViewModel(
input: AppInfo,
selectedPatches: PatchesSelection
) : ViewModel(), KoinComponent {
private val signerService: SignerService by inject()
private val keystoreManager: KeystoreManager by inject()
private val app: Application by inject()
private val pm: PM by inject()

Expand Down Expand Up @@ -102,7 +104,8 @@ class InstallerViewModel(

if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
app.toast(app.getString(R.string.install_app_success))
installedPackageName = intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
installedPackageName =
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
} else {
app.toast(app.getString(R.string.install_app_fail, extra))
}
Expand Down Expand Up @@ -134,9 +137,9 @@ class InstallerViewModel(
private fun signApk(): Boolean {
if (!hasSigned) {
try {
signerService.createSigner().signApk(outputFile, signedFile)
} catch (e: Throwable) {
e.printStackTrace()
keystoreManager.sign(outputFile, signedFile)
} catch (e: Exception) {
Log.e(tag, "Got exception while signing", e)
app.toast(app.getString(R.string.sign_fail, e::class.simpleName))
return false
}
Expand Down
Loading

0 comments on commit 919b6b7

Please sign in to comment.