From 35eaae73a8089381b5440bdaf17f473b344638f1 Mon Sep 17 00:00:00 2001 From: zt64 Date: Sun, 13 Mar 2022 11:39:21 -0400 Subject: [PATCH] Use apksig for signing and other minor changes --- app/build.gradle.kts | 7 +- .../manager/ui/components/TextField.kt | 3 +- .../manager/ui/screens/InstallScreen.kt | 13 +- .../manager/ui/screens/SettingsScreen.kt | 24 +-- .../manager/ui/screens/StoreScreen.kt | 1 + .../com/aliucord/manager/utils/Const.kt | 17 +- .../aliucord/manager/utils/DownloadUtils.kt | 2 +- .../com/aliucord/manager/utils/Signer.kt | 190 ++++-------------- 8 files changed, 71 insertions(+), 186 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f95879..b495863 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/TextField.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/TextField.kt index b14fe54..a77a4e5 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/TextField.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/TextField.kt @@ -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 diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt index 5c5e7a9..192b55e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt @@ -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) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt index 6d48e1e..c6c8728 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt @@ -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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt index 30e7ff7..9aaab14 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt @@ -36,6 +36,7 @@ private data class Tree( fun StoreScreen() { val plugins = remember { mutableStateListOf() } + // TODO: Display cards with option to install plugin Column { if (plugins.isEmpty()) { Box( diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/Const.kt b/app/src/main/kotlin/com/aliucord/manager/utils/Const.kt index d1c85b8..8fd59ec 100644 --- a/app/src/main/kotlin/com/aliucord/manager/utils/Const.kt +++ b/app/src/main/kotlin/com/aliucord/manager/utils/Const.kt @@ -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() diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt b/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt index 1468cac..dbdd320 100644 --- a/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt +++ b/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt @@ -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) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/Signer.kt b/app/src/main/kotlin/com/aliucord/manager/utils/Signer.kt index 41effcd..a2d7984 100644 --- a/app/src/main/kotlin/com/aliucord/manager/utils/Signer.kt +++ b/app/src/main/kotlin/com/aliucord/manager/utils/Signer.kt @@ -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(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() - val filesToRemove = arrayListOf() - - 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() - 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)) }