Use apksig for signing and other minor changes
This commit is contained in:
parent
513e820e70
commit
35eaae73a8
8 changed files with 71 additions and 186 deletions
|
|
@ -58,18 +58,18 @@ kotlin {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree("./libs"))
|
||||
// implementation(fileTree("./libs"))
|
||||
// core dependencies
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
// implementation("com.github.aliucord:libzip:1.0.1")
|
||||
implementation("com.github.aliucord:libzip:1.0.1")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
||||
|
||||
// compose dependencies
|
||||
implementation("androidx.compose.ui:ui:${composeVersion}")
|
||||
implementation("androidx.compose.ui:ui-tooling:${composeVersion}")
|
||||
implementation("androidx.compose.material:material:${composeVersion}")
|
||||
implementation("androidx.compose.material3:material3:1.0.0-alpha06")
|
||||
implementation("androidx.compose.material3:material3:1.0.0-alpha07")
|
||||
implementation("androidx.paging:paging-compose:1.0.0-alpha14")
|
||||
|
||||
// accompanist dependencies
|
||||
|
|
@ -85,6 +85,7 @@ dependencies {
|
|||
implementation("io.coil-kt:coil-compose:1.4.0")
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.65")
|
||||
implementation("de.upb.cs.swt:axml:2.1.1") // 2.1.2 is broken btw
|
||||
implementation("com.android.tools.build:apksig:7.1.2")
|
||||
|
||||
// ktor
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) {
|
|||
val discordApk = apk ?: run {
|
||||
log += "Checking for cached APK...\n"
|
||||
|
||||
File(context.externalCacheDir, "discord-${supportedVersion}.apk").also { file ->
|
||||
context.externalCacheDir!!.resolve("discord-${supportedVersion}.apk").also { file ->
|
||||
val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0)
|
||||
|
||||
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also
|
||||
|
|
@ -52,7 +52,7 @@ fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) {
|
|||
}
|
||||
}
|
||||
|
||||
val injector = File(context.externalCacheDir, "Injector.dex").also { file ->
|
||||
val injector = context.externalCacheDir!!.resolve("Injector.dex").also { file ->
|
||||
if (file.exists()) return@also
|
||||
|
||||
log += "Downloading injector...\n"
|
||||
|
|
@ -66,7 +66,7 @@ fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) {
|
|||
if (!it.exists() && !it.mkdirs()) throw FileNotFoundException()
|
||||
}
|
||||
|
||||
val outputApk = File(outputDir, "Aliucord.apk")
|
||||
val outputApk = outputDir.resolve("Aliucord.apk")
|
||||
discordApk.copyTo(outputApk, true)
|
||||
|
||||
log += "Repacking APK\n"
|
||||
|
|
@ -118,7 +118,7 @@ fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) {
|
|||
|
||||
if (!patched) for (i in 2..4) {
|
||||
val name = "classes$i.dex"
|
||||
val cacheFile = File(context.cacheDir, name)
|
||||
val cacheFile = context.cacheDir.resolve(name)
|
||||
|
||||
openEntry(name)
|
||||
compressFile(cacheFile.absolutePath)
|
||||
|
|
@ -179,13 +179,12 @@ fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) {
|
|||
log += "Signed APK\n"
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
|
||||
setDataAndType(
|
||||
FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", outputApk),
|
||||
"application/vnd.android.package-archive"
|
||||
)
|
||||
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
context.startActivity(intent)
|
||||
|
|
|
|||
|
|
@ -10,11 +10,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -63,17 +59,11 @@ fun SettingsScreen() = Column(
|
|||
|
||||
TextField(
|
||||
value = Prefs.appName.get(),
|
||||
onValueChange = { Prefs.appName.set(it) },
|
||||
onValueChange = Prefs.appName::set,
|
||||
placeholder = { Text(stringResource(R.string.app_name_setting)) },
|
||||
label = { Text(stringResource(R.string.app_name_setting)) },
|
||||
)
|
||||
|
||||
TextField(
|
||||
value = Prefs.packageName.get(),
|
||||
onValueChange = { Prefs.packageName.set(it) },
|
||||
placeholder = { Text(stringResource(R.string.package_name)) },
|
||||
label = { Text(stringResource(R.string.package_name)) },
|
||||
)
|
||||
|
||||
val devModeOn = Prefs.devMode.get()
|
||||
|
||||
|
|
@ -83,6 +73,13 @@ fun SettingsScreen() = Column(
|
|||
)
|
||||
|
||||
if (devModeOn) {
|
||||
TextField(
|
||||
value = Prefs.packageName.get(),
|
||||
onValueChange = Prefs.packageName::set,
|
||||
placeholder = { Text(stringResource(R.string.package_name)) },
|
||||
label = { Text(stringResource(R.string.package_name)) },
|
||||
)
|
||||
|
||||
PreferenceItem(
|
||||
title = { Text(stringResource(R.string.debuggable)) },
|
||||
description = { Text(stringResource(R.string.debuggable_description)) },
|
||||
|
|
@ -106,6 +103,7 @@ fun SettingsScreen() = Column(
|
|||
MaterialTheme.colorScheme.onBackground.copy(ContentAlpha.disabled)
|
||||
}
|
||||
|
||||
// TODO: Make installer use selected dex file
|
||||
ListItem(
|
||||
modifier = if (udfs.get()) Modifier.clickable {
|
||||
picker.launch(arrayOf("application/octet-stream"))
|
||||
|
|
@ -119,9 +117,7 @@ fun SettingsScreen() = Column(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
onClick = {
|
||||
context.cacheDir.deleteRecursively()
|
||||
}
|
||||
onClick = context.cacheDir::deleteRecursively
|
||||
) {
|
||||
Text(stringResource(R.string.clear_files_cache), textAlign = TextAlign.Center)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ private data class Tree(
|
|||
fun StoreScreen() {
|
||||
val plugins = remember { mutableStateListOf<Tree.File>() }
|
||||
|
||||
// TODO: Display cards with option to install plugin
|
||||
Column {
|
||||
if (plugins.isEmpty()) {
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -10,16 +10,15 @@ import com.google.gson.Gson
|
|||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.android.*
|
||||
import io.ktor.client.features.json.*
|
||||
import java.io.File
|
||||
|
||||
val httpClient = HttpClient(Android) {
|
||||
install(JsonFeature)
|
||||
}
|
||||
val aliucordDir = File(Environment.getExternalStorageDirectory(), "Aliucord")
|
||||
val pluginsDir = File(aliucordDir, "plugins")
|
||||
val gson = Gson()
|
||||
|
||||
const val MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"
|
||||
const val REQUEST_LEGACY_STORAGE = "requestLegacyExternalStorage"
|
||||
const val DEBUGGABLE = "debugabble"
|
||||
const val DEBUGGABLE = "debuggable"
|
||||
|
||||
val httpClient = HttpClient(Android) {
|
||||
install(JsonFeature)
|
||||
}
|
||||
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")
|
||||
val pluginsDir = aliucordDir.resolve("plugins")
|
||||
val gson = Gson()
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ object DownloadUtils {
|
|||
|
||||
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||
setDestinationUri(File(ctx.externalCacheDir, fileName).toUri())
|
||||
setDestinationUri(ctx.externalCacheDir!!.resolve(fileName).toUri())
|
||||
downloadManager.enqueue(this)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,36 +5,47 @@
|
|||
|
||||
package com.aliucord.manager.utils
|
||||
|
||||
import com.aliucord.libzip.Zip
|
||||
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.JcaCertStore
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray
|
||||
import org.bouncycastle.cms.CMSSignedDataGenerator
|
||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder
|
||||
import org.bouncycastle.util.encoders.Base64
|
||||
import java.io.*
|
||||
import java.io.File
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.jar.*
|
||||
|
||||
private class KeySet(val publicKey: X509Certificate, val privateKey: PrivateKey)
|
||||
|
||||
object Signer {
|
||||
private val stripPattern = Regex("^META-INF/(.*)[.](MF|SF|RSA|DSA)$")
|
||||
private val password = "password".toCharArray()
|
||||
|
||||
private val signerConfig: ApkSigner.SignerConfig by lazy {
|
||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
|
||||
aliucordDir.resolve("ks.keystore").also {
|
||||
if (!it.exists()) newKeystore(it)
|
||||
}.inputStream().use { stream ->
|
||||
keyStore.load(stream, null)
|
||||
}
|
||||
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
val certificate = keyStore.getCertificate(alias) as X509Certificate
|
||||
|
||||
ApkSigner.SignerConfig.Builder(
|
||||
"Aliucord Manager signer",
|
||||
keyStore.getKey(alias, password) as PrivateKey,
|
||||
listOf(certificate),
|
||||
).build()
|
||||
}
|
||||
|
||||
private fun newKeystore(out: File?) {
|
||||
val password = "password".toCharArray()
|
||||
val key = createKey()
|
||||
|
||||
with(KeyStore.getInstance("BKS", "BC")) {
|
||||
with(KeyStore.getInstance(KeyStore.getDefaultType())) {
|
||||
load(null, password)
|
||||
setKeyEntry("alias", key.privateKey, password, arrayOf<Certificate>(key.publicKey))
|
||||
store(out?.outputStream(), password)
|
||||
|
|
@ -42,17 +53,16 @@ object Signer {
|
|||
}
|
||||
|
||||
private fun createKey(): KeySet {
|
||||
val pair = KeyPairGenerator.getInstance("RSA").run {
|
||||
initialize(2048)
|
||||
generateKeyPair()
|
||||
}
|
||||
|
||||
var serialNumber: BigInteger
|
||||
|
||||
do serialNumber = SecureRandom().nextInt().toBigInteger()
|
||||
while (serialNumber < BigInteger.ZERO)
|
||||
|
||||
val x500Name = X500Name("CN=Aliucord Installer")
|
||||
val x500Name = X500Name("CN=Aliucord Manager")
|
||||
val pair = KeyPairGenerator.getInstance("RSA").run {
|
||||
initialize(2048)
|
||||
generateKeyPair()
|
||||
}
|
||||
val builder = X509v3CertificateBuilder(
|
||||
x500Name,
|
||||
serialNumber,
|
||||
|
|
@ -67,139 +77,17 @@ object Signer {
|
|||
return KeySet(JcaX509CertificateConverter().getCertificate(builder.build(signer)), pair.private)
|
||||
}
|
||||
|
||||
// based on https://gist.github.com/mmuszkow/10288441
|
||||
// and https://github.com/fornwall/apksigner/blob/master/src/main/java/net/fornwall/apksigner/ZipSigner.java
|
||||
@Throws(Exception::class)
|
||||
fun signApk(apkFile: File) {
|
||||
val ks = File(apkFile.parent, "ks.keystore").also { keyStore ->
|
||||
if (!keyStore.exists()) newKeystore(keyStore)
|
||||
}
|
||||
|
||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||
ks.inputStream().use { keyStore.load(it, null) }
|
||||
|
||||
val alias = keyStore.aliases().nextElement()
|
||||
val password = "password".toCharArray()
|
||||
val keySet = KeySet(
|
||||
keyStore.getCertificate(alias) as X509Certificate,
|
||||
keyStore.getKey(alias, password) as PrivateKey
|
||||
)
|
||||
var zip = Zip(apkFile.absolutePath, 6, 'r')
|
||||
val dig = MessageDigest.getInstance("SHA1")
|
||||
val digests = linkedMapOf<String, String>()
|
||||
val filesToRemove = arrayListOf<String>()
|
||||
|
||||
repeat(zip.totalEntries) { i ->
|
||||
zip.openEntryByIndex(i)
|
||||
|
||||
val name = zip.entryName
|
||||
|
||||
if (stripPattern.matches(name))
|
||||
filesToRemove.add(name)
|
||||
else
|
||||
digests[name] = dig.digest(zip.readEntry()).toBase64()
|
||||
|
||||
zip.closeEntry()
|
||||
}
|
||||
zip.close()
|
||||
zip = Zip(apkFile.absolutePath, 6, 'a')
|
||||
|
||||
filesToRemove.forEach(zip::deleteEntry)
|
||||
val sectionDigests = LinkedHashMap<String, String>()
|
||||
var manifest = Manifest()
|
||||
var attrs = manifest.mainAttributes
|
||||
attrs[Attributes.Name.MANIFEST_VERSION] = "1.0"
|
||||
attrs[Attributes.Name("Created-By")] = "Aliucord Installer"
|
||||
val digestAttr = Attributes.Name("SHA1-Digest")
|
||||
|
||||
digests.forEach { (name, value) ->
|
||||
val attributes = Attributes()
|
||||
|
||||
attributes[digestAttr] = value
|
||||
manifest.entries[name] = attributes
|
||||
sectionDigests[name] = hashEntrySection(name, attributes, dig)
|
||||
}
|
||||
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
|
||||
zip.openEntry(JarFile.MANIFEST_NAME)
|
||||
zip.writeEntry(baos.toByteArray(), baos.size().toLong())
|
||||
zip.closeEntry()
|
||||
}
|
||||
|
||||
val manifestHash = getManifestHash(manifest, dig)
|
||||
val tmpManifest = Manifest().apply {
|
||||
mainAttributes.putAll(attrs)
|
||||
}
|
||||
val manifestMainHash = getManifestHash(tmpManifest, dig)
|
||||
|
||||
manifest = Manifest()
|
||||
attrs = manifest.mainAttributes
|
||||
attrs[Attributes.Name.SIGNATURE_VERSION] = "1.0"
|
||||
attrs[Attributes.Name("Created-By")] = "Aliucord Installer"
|
||||
attrs[Attributes.Name("SHA1-Digest-Manifest")] = manifestHash
|
||||
attrs[Attributes.Name("SHA1-Digest-Manifest-Main-Attributes")] = manifestMainHash
|
||||
sectionDigests.forEach { (key, value) ->
|
||||
val attributes = Attributes()
|
||||
attributes[digestAttr] = value
|
||||
manifest.entries[key] = attributes
|
||||
}
|
||||
|
||||
var sigBytes: ByteArray
|
||||
|
||||
ByteArrayOutputStream().use { sigStream ->
|
||||
manifest.write(sigStream)
|
||||
sigBytes = sigStream.toByteArray()
|
||||
zip.openEntry("META-INF/CERT.SF")
|
||||
zip.writeEntry(sigBytes, sigStream.size().toLong())
|
||||
zip.closeEntry()
|
||||
}
|
||||
|
||||
val signature = signSigFile(keySet, sigBytes)
|
||||
zip.openEntry("META-INF/CERT.RSA")
|
||||
zip.writeEntry(signature, signature.size.toLong())
|
||||
zip.closeEntry()
|
||||
zip.close()
|
||||
val outputApk = aliucordDir.resolve("Aliucord-Signed.apk")
|
||||
ApkSigner.Builder(listOf(signerConfig))
|
||||
.setV1SigningEnabled(false)
|
||||
.setV2SigningEnabled(true)
|
||||
.setV3SigningEnabled(false)
|
||||
.setOtherSignersSignaturesPreserved(false)
|
||||
.setInputApk(apkFile)
|
||||
.setOutputApk(outputApk)
|
||||
.build()
|
||||
.sign()
|
||||
outputApk.renameTo(apkFile)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun hashEntrySection(name: String, attrs: Attributes, dig: MessageDigest): String {
|
||||
val manifest = Manifest()
|
||||
manifest.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0"
|
||||
|
||||
ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
|
||||
val emptyLen = baos.toByteArray().size
|
||||
manifest.entries[name] = attrs
|
||||
baos.reset()
|
||||
manifest.write(baos)
|
||||
var ob = baos.toByteArray()
|
||||
ob = ob.copyOfRange(emptyLen, ob.size)
|
||||
return dig.digest(ob).toBase64()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getManifestHash(manifest: Manifest, dig: MessageDigest) = ByteArrayOutputStream().use { baos ->
|
||||
manifest.write(baos)
|
||||
dig.digest(baos.toByteArray()).toBase64()
|
||||
}
|
||||
|
||||
private fun signSigFile(keySet: KeySet, content: ByteArray): ByteArray {
|
||||
val sha1Signer = JcaContentSignerBuilder("SHA1withRSA").build(keySet.privateKey)
|
||||
val jcaDigestCalculatorProviderBuilder = JcaDigestCalculatorProviderBuilder()
|
||||
val jcaSignerInfoGeneratorBuilder =
|
||||
JcaSignerInfoGeneratorBuilder(jcaDigestCalculatorProviderBuilder.build()).apply {
|
||||
setDirectSignature(true)
|
||||
}
|
||||
val sigData = CMSSignedDataGenerator().apply {
|
||||
addSignerInfoGenerator(jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.publicKey))
|
||||
addCertificates(JcaCertStore(listOf(keySet.publicKey)))
|
||||
}.generate(CMSProcessableByteArray(content), false)
|
||||
return sigData.toASN1Structure().getEncoded("DER")
|
||||
}
|
||||
|
||||
private fun ByteArray.toBase64() = String(Base64.encode(this))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue