mirror of
https://github.com/revanced/revanced-manager-compose.git
synced 2025-04-30 06:14:25 +02:00
feat: improve keystore UI and UX (#52)
This commit is contained in:
parent
36b05097fc
commit
f862e82b33
@ -8,26 +8,24 @@ import java.io.File
|
|||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
import kotlin.io.path.exists
|
import kotlin.io.path.exists
|
||||||
|
|
||||||
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Default common name and password for the keystore.
|
* Default alias and password for the keystore.
|
||||||
*/
|
*/
|
||||||
const val DEFAULT = "ReVanced"
|
const val DEFAULT = "ReVanced"
|
||||||
|
|
||||||
/**
|
|
||||||
* The default password used by the Flutter version.
|
|
||||||
*/
|
|
||||||
const val FLUTTER_MANAGER_PASSWORD = "s3cur3p@ssw0rd"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val keystorePath = app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath()
|
private val keystorePath =
|
||||||
|
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath()
|
||||||
|
|
||||||
private fun options(
|
private fun options(
|
||||||
cn: String = prefs.keystoreCommonName!!,
|
cn: String = prefs.keystoreCommonName!!,
|
||||||
pass: String = prefs.keystorePass!!
|
pass: String = prefs.keystorePass!!,
|
||||||
) = SigningOptions(cn, pass, keystorePath)
|
) = SigningOptions(cn, pass, keystorePath)
|
||||||
|
|
||||||
private fun updatePrefs(cn: String, pass: String) {
|
private fun updatePrefs(cn: String, pass: String) {
|
||||||
@ -47,11 +45,14 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
|||||||
updatePrefs(DEFAULT, DEFAULT)
|
updatePrefs(DEFAULT, DEFAULT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun import(cn: String, pass: String, keystore: InputStream) {
|
fun import(cn: String, pass: String, keystore: Path): Boolean {
|
||||||
// TODO: check if the user actually provided the correct password
|
if (!Signer(SigningOptions(cn, pass, keystore)).canUnlock()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)
|
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
|
||||||
updatePrefs(cn, pass)
|
updatePrefs(cn, pass)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun export(target: OutputStream) {
|
fun export(target: OutputStream) {
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Visibility
|
||||||
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null) {
|
||||||
|
var visible by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
placeholder = placeholder,
|
||||||
|
label = label,
|
||||||
|
modifier = modifier,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
visible = !visible
|
||||||
|
}) {
|
||||||
|
val (icon, description) = remember(visible) {
|
||||||
|
if (visible) Icons.Outlined.VisibilityOff to R.string.hide_password_field else Icons.Outlined.Visibility to R.string.show_password_field
|
||||||
|
}
|
||||||
|
Icon(icon, stringResource(description))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Password
|
||||||
|
),
|
||||||
|
visualTransformation = if (visible) VisualTransformation.None else PasswordVisualTransformation()
|
||||||
|
)
|
||||||
|
}
|
@ -1,35 +1,38 @@
|
|||||||
package app.revanced.manager.ui.screen.settings
|
package app.revanced.manager.ui.screen.settings
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
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.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Key
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
|
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.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ContentSelector
|
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
|
import app.revanced.manager.ui.component.PasswordField
|
||||||
import app.revanced.manager.ui.component.sources.SourceSelector
|
import app.revanced.manager.ui.component.sources.SourceSelector
|
||||||
|
import app.revanced.manager.util.toast
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
import org.koin.compose.rememberKoinInject
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@ -37,8 +40,14 @@ fun ImportExportSettingsScreen(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: ImportExportViewModel = getViewModel()
|
vm: ImportExportViewModel = getViewModel()
|
||||||
) {
|
) {
|
||||||
var showImportKeystoreDialog by rememberSaveable { mutableStateOf(false) }
|
val importKeystoreLauncher =
|
||||||
var showExportKeystoreDialog by rememberSaveable { mutableStateOf(false) }
|
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
|
||||||
|
it?.let { uri -> vm.startKeystoreImport(uri) }
|
||||||
|
}
|
||||||
|
val exportKeystoreLauncher =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) {
|
||||||
|
it?.let(vm::exportKeystore)
|
||||||
|
}
|
||||||
|
|
||||||
vm.selectionAction?.let { action ->
|
vm.selectionAction?.let { action ->
|
||||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
@ -62,16 +71,10 @@ fun ImportExportSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showImportKeystoreDialog) {
|
if (vm.showCredentialsDialog) {
|
||||||
ImportKeystoreDialog(
|
KeystoreCredentialsDialog(
|
||||||
onDismissRequest = { showImportKeystoreDialog = false },
|
onDismissRequest = vm::cancelKeystoreImport,
|
||||||
onImport = vm::importKeystore
|
tryImport = vm::tryKeystoreImport
|
||||||
)
|
|
||||||
}
|
|
||||||
if (showExportKeystoreDialog) {
|
|
||||||
ExportKeystoreDialog(
|
|
||||||
onDismissRequest = { showExportKeystoreDialog = false },
|
|
||||||
onExport = vm::exportKeystore
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,14 +95,14 @@ fun ImportExportSettingsScreen(
|
|||||||
GroupHeader(stringResource(R.string.signing))
|
GroupHeader(stringResource(R.string.signing))
|
||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
showImportKeystoreDialog = true
|
importKeystoreLauncher.launch("*/*")
|
||||||
},
|
},
|
||||||
headline = R.string.import_keystore,
|
headline = R.string.import_keystore,
|
||||||
description = R.string.import_keystore_descripion
|
description = R.string.import_keystore_descripion
|
||||||
)
|
)
|
||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
showExportKeystoreDialog = true
|
exportKeystoreLauncher.launch("Manager.keystore")
|
||||||
},
|
},
|
||||||
headline = R.string.export_keystore,
|
headline = R.string.export_keystore,
|
||||||
description = R.string.export_keystore_description
|
description = R.string.export_keystore_description
|
||||||
@ -139,90 +142,64 @@ private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExportKeystoreDialog(
|
fun KeystoreCredentialsDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onExport: (Uri) -> Unit
|
tryImport: (String, String) -> Boolean
|
||||||
) {
|
) {
|
||||||
val activityLauncher =
|
val context = LocalContext.current
|
||||||
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { uri ->
|
var cn by rememberSaveable { mutableStateOf("") }
|
||||||
uri?.let {
|
var pass by rememberSaveable { mutableStateOf("") }
|
||||||
onExport(it)
|
|
||||||
onDismissRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val prefs: PreferencesManager = rememberKoinInject()
|
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Button(
|
TextButton(
|
||||||
onClick = { activityLauncher.launch("Manager.keystore") }
|
onClick = {
|
||||||
) {
|
if (!tryImport(
|
||||||
Text(stringResource(R.string.select_file))
|
cn,
|
||||||
}
|
pass
|
||||||
},
|
|
||||||
title = { Text(stringResource(R.string.export_keystore)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
Text("Current common name: ${prefs.keystoreCommonName}")
|
|
||||||
Text("Current password: ${prefs.keystorePass}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
|
||||||
|
|
||||||
@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 = {
|
|
||||||
ContentSelector(
|
|
||||||
mime = "*/*",
|
|
||||||
onSelect = {
|
|
||||||
onImport(it, cn, pass)
|
|
||||||
onDismissRequest()
|
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.select_file))
|
Text(stringResource(R.string.import_keystore_dialog_button))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = { Text(stringResource(R.string.import_keystore)) },
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(Icons.Outlined.Key, null)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.import_keystore_dialog_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column(
|
||||||
TextField(
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.import_keystore_dialog_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
value = cn,
|
value = cn,
|
||||||
onValueChange = { cn = it },
|
onValueChange = { cn = it },
|
||||||
label = { Text("Common Name") }
|
label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) }
|
||||||
)
|
)
|
||||||
TextField(
|
PasswordField(
|
||||||
value = pass,
|
value = pass,
|
||||||
onValueChange = { pass = it },
|
onValueChange = { pass = it },
|
||||||
label = { Text("Password") }
|
label = { Text(stringResource(R.string.import_keystore_dialog_password_field)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
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
|
||||||
@ -25,6 +26,11 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import kotlinx.serialization.json.encodeToStream
|
import kotlinx.serialization.json.encodeToStream
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import kotlin.io.path.deleteExisting
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
class ImportExportViewModel(
|
class ImportExportViewModel(
|
||||||
@ -39,9 +45,48 @@ class ImportExportViewModel(
|
|||||||
private set
|
private set
|
||||||
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
||||||
private set
|
private set
|
||||||
|
private var keystoreImportPath by mutableStateOf<Path?>(null)
|
||||||
|
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
|
||||||
|
|
||||||
fun importKeystore(content: Uri, cn: String, pass: String) =
|
fun startKeystoreImport(content: Uri) {
|
||||||
keystoreManager.import(cn, pass, contentResolver.openInputStream(content)!!)
|
val path = File.createTempFile("signing", "ks", app.cacheDir).toPath()
|
||||||
|
Files.copy(
|
||||||
|
contentResolver.openInputStream(content)!!,
|
||||||
|
path,
|
||||||
|
StandardCopyOption.REPLACE_EXISTING
|
||||||
|
)
|
||||||
|
|
||||||
|
knownPasswords.forEach {
|
||||||
|
if (tryKeystoreImport(KeystoreManager.DEFAULT, it, path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keystoreImportPath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelKeystoreImport() {
|
||||||
|
keystoreImportPath?.deleteExisting()
|
||||||
|
keystoreImportPath = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryKeystoreImport(cn: String, pass: String) =
|
||||||
|
tryKeystoreImport(cn, pass, keystoreImportPath!!)
|
||||||
|
|
||||||
|
private fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
|
||||||
|
if (keystoreManager.import(cn, pass, path)) {
|
||||||
|
cancelKeystoreImport()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
|
||||||
|
cancelKeystoreImport()
|
||||||
|
}
|
||||||
|
|
||||||
fun exportKeystore(target: Uri) =
|
fun exportKeystore(target: Uri) =
|
||||||
keystoreManager.export(contentResolver.openOutputStream(target)!!)
|
keystoreManager.export(contentResolver.openOutputStream(target)!!)
|
||||||
@ -120,4 +165,8 @@ class ImportExportViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val knownPasswords = setOf("ReVanced", "s3cur3p@ssw0rd")
|
||||||
|
}
|
||||||
}
|
}
|
@ -11,6 +11,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|||||||
import org.bouncycastle.operator.ContentSigner
|
import org.bouncycastle.operator.ContentSigner
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.*
|
import java.security.*
|
||||||
@ -55,16 +56,33 @@ class Signer(
|
|||||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
||||||
}
|
}
|
||||||
|
|
||||||
fun signApk(input: File, output: File) {
|
private fun loadKeystore(): KeyStore {
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
|
|
||||||
val ks = signingOptions.keyStoreFilePath
|
val ks = signingOptions.keyStoreFilePath
|
||||||
if (!ks.exists()) newKeystore(ks) else {
|
if (!ks.exists()) newKeystore(ks) else {
|
||||||
Log.i(tag, "Found existing keystore: ${ks.name}")
|
Log.i(tag, "Found existing keystore: ${ks.name}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Security.addProvider(BouncyCastleProvider())
|
||||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||||
ks.inputStream().use { stream -> keyStore.load(stream, null) }
|
ks.inputStream().use { keyStore.load(it, null) }
|
||||||
|
return keyStore
|
||||||
|
}
|
||||||
|
|
||||||
|
fun canUnlock(): Boolean {
|
||||||
|
val keyStore = loadKeystore()
|
||||||
|
val alias = keyStore.aliases().nextElement()
|
||||||
|
|
||||||
|
try {
|
||||||
|
keyStore.getKey(alias, passwordCharArray)
|
||||||
|
} catch (_: UnrecoverableKeyException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun signApk(input: File, output: File) {
|
||||||
|
val keyStore = loadKeystore()
|
||||||
val alias = keyStore.aliases().nextElement()
|
val alias = keyStore.aliases().nextElement()
|
||||||
|
|
||||||
val config = ApkSigner.SignerConfig.Builder(
|
val config = ApkSigner.SignerConfig.Builder(
|
||||||
|
@ -36,8 +36,12 @@
|
|||||||
<string name="experimental_patches_description">Allow patching incompatible patches with experimental versions, something may break</string>
|
<string name="experimental_patches_description">Allow patching incompatible patches with experimental versions, something may break</string>
|
||||||
<string name="import_keystore">Import keystore</string>
|
<string name="import_keystore">Import keystore</string>
|
||||||
<string name="import_keystore_descripion">Import a custom keystore</string>
|
<string name="import_keystore_descripion">Import a custom keystore</string>
|
||||||
<string name="import_keystore_preset_default">Default</string>
|
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
|
||||||
<string name="import_keystore_preset_flutter">ReVanced Manager (Flutter)</string>
|
<string name="import_keystore_dialog_description">You\'ll need enter the keystore’s credentials to import it.</string>
|
||||||
|
<string name="import_keystore_dialog_alias_field">Username (Alias)</string>
|
||||||
|
<string name="import_keystore_dialog_password_field">Password</string>
|
||||||
|
<string name="import_keystore_dialog_button">Import</string>
|
||||||
|
<string name="import_keystore_wrong_credentials">Wrong keystore credentials</string>
|
||||||
<string name="export_keystore">Export keystore</string>
|
<string name="export_keystore">Export keystore</string>
|
||||||
<string name="export_keystore_description">Export the current keystore</string>
|
<string name="export_keystore_description">Export the current keystore</string>
|
||||||
<string name="regenerate_keystore">Regenerate keystore</string>
|
<string name="regenerate_keystore">Regenerate keystore</string>
|
||||||
@ -97,6 +101,9 @@
|
|||||||
|
|
||||||
<string name="select_file">Select file</string>
|
<string name="select_file">Select file</string>
|
||||||
|
|
||||||
|
<string name="show_password_field">Show password</string>
|
||||||
|
<string name="hide_password_field">Hide password</string>
|
||||||
|
|
||||||
<string name="installer">Installer</string>
|
<string name="installer">Installer</string>
|
||||||
<string name="install_app">Install</string>
|
<string name="install_app">Install</string>
|
||||||
<string name="install_app_success">App installed</string>
|
<string name="install_app_success">App installed</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user