Use apksig for signing and other minor changes

This commit is contained in:
zt64 2022-03-13 11:39:21 -04:00
parent 513e820e70
commit 35eaae73a8
8 changed files with 71 additions and 186 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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