Skip to content

Commit 48924e4

Browse files
committed
feat(patcher): inject new CA certificates
Old Android devices do not have the new CA root certificates, and fixing this user-side requires manually installing them as system certificates, requiring root.
1 parent aefb459 commit 48924e4

File tree

9 files changed

+249
-16
lines changed

9 files changed

+249
-16
lines changed

.editorconfig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ ij_java_keep_simple_methods_in_one_line = true
2525
ij_java_line_comment_add_space = true
2626
ij_java_line_comment_at_first_column = false
2727
ij_java_spaces_within_array_initializer_braces = true
28+
ij_kotlin_call_parameters_new_line_after_left_paren = false
29+
ij_kotlin_call_parameters_right_paren_on_new_line = false
2830

2931
[*.kt]
3032
ij_kotlin_name_count_to_use_star_import = 3

app/src/main/kotlin/com/aliucord/manager/patcher/KotlinPatchRunner.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class KotlinPatchRunner(
3131
SmaliPatchStep(),
3232
PatchIconsStep(options),
3333
PatchManifestStep(options),
34+
PatchCertsStep(),
3435
ReorganizeDexStep(),
3536
AddAliuhookLibsStep(),
3637
SaveMetadataStep(options),
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package com.aliucord.manager.patcher.steps.patch
2+
3+
import android.app.Application
4+
import android.os.Build
5+
import com.aliucord.manager.R
6+
import com.aliucord.manager.patcher.StepRunner
7+
import com.aliucord.manager.patcher.steps.StepGroup
8+
import com.aliucord.manager.patcher.steps.base.Step
9+
import com.aliucord.manager.patcher.steps.base.StepState
10+
import com.aliucord.manager.patcher.steps.download.CopyDependenciesStep
11+
import com.aliucord.manager.patcher.util.ArscUtil
12+
import com.aliucord.manager.patcher.util.ArscUtil.addResource
13+
import com.aliucord.manager.patcher.util.ArscUtil.getMainArscChunk
14+
import com.aliucord.manager.patcher.util.ArscUtil.getPackageChunk
15+
import com.aliucord.manager.patcher.util.ArscUtil.getResourceFileNames
16+
import com.aliucord.manager.patcher.util.AxmlUtil.getMainAxmlChunk
17+
import com.aliucord.manager.util.find
18+
import com.github.diamondminer88.zip.ZipReader
19+
import com.github.diamondminer88.zip.ZipWriter
20+
import com.google.devrel.gmscore.tools.apk.arsc.*
21+
import org.koin.core.component.KoinComponent
22+
import org.koin.core.component.inject
23+
import java.io.File
24+
25+
/**
26+
* Adds a network security config that manually adds new CA root certificates.
27+
* This is useful for old Android devices that do not have updated root certs.
28+
*/
29+
class PatchCertsStep : Step(), KoinComponent {
30+
private val context: Application by inject()
31+
32+
override val group = StepGroup.Patch
33+
override val localizedName = R.string.patch_step_patch_certs
34+
35+
// Manager's (this application) network security config is used as a template
36+
// to inject into Aliucord, except with the resource ids pointing to certificate files changed
37+
// to new ones injected into the patched app's arsc
38+
override suspend fun execute(container: StepRunner) {
39+
val apk = container.getStep<CopyDependenciesStep>().patchedApk
40+
41+
if (Build.VERSION.SDK_INT >= 26) {
42+
container.log("Modern device detected, skipping injecting root certs")
43+
state = StepState.Skipped
44+
return
45+
}
46+
47+
container.log("Parsing resources.arsc")
48+
val arsc = ArscUtil.readArsc(apk)
49+
val resourcesChunk = arsc.getMainArscChunk()
50+
val packageChunk = arsc.getPackageChunk()
51+
52+
container.log("Creating new raw resources in arsc")
53+
val certificateIds = CERTIFICATES.keys.map { certificateName ->
54+
packageChunk.addResource(
55+
typeName = "raw",
56+
resourceName = certificateName,
57+
configurations = { it.isDefault },
58+
valueType = BinaryResourceValue.Type.STRING,
59+
valueData = resourcesChunk.stringPool.addString("res/$certificateName.der"),
60+
)
61+
}
62+
63+
container.log("Generating new network security config AXML")
64+
val newNetworkSecurityConfigBytes = generateNetworkConfig(certificateIds)
65+
66+
container.log("Parsing existing AndroidManifest.xml")
67+
val networkSecurityConfigId = getNetworkSecurityConfigResourceId(apk)
68+
val networkSecurityConfigPath = resourcesChunk.getResourceFileNames(
69+
resourceId = networkSecurityConfigId,
70+
configurations = { it.isDefault },
71+
).single()
72+
73+
ZipWriter(apk, /* append = */ true).use { zip ->
74+
zip.deleteEntries(networkSecurityConfigPath, "resources.arsc")
75+
76+
container.log("Writing new network security config AXML")
77+
zip.writeEntry(networkSecurityConfigPath, newNetworkSecurityConfigBytes)
78+
79+
container.log("Writing new arsc")
80+
zip.writeEntry("resources.arsc", arsc.toByteArray())
81+
82+
for ((name, id) in CERTIFICATES) {
83+
container.log("Writing $name CA certificate to apk")
84+
85+
val bytes = context.resources.openRawResource(id).use { it.readBytes() }
86+
zip.writeEntry("res/$name.der", bytes)
87+
}
88+
}
89+
}
90+
91+
/**
92+
* From an APK, read the manifest's `android:networkSecurityConfig` references to a resource.
93+
* This is then used to get the filename of the resource from `resources.arsc`.
94+
*/
95+
fun getNetworkSecurityConfigResourceId(apk: File): BinaryResourceIdentifier {
96+
val manifestBytes = ZipReader(apk).use {
97+
it.openEntry("AndroidManifest.xml")?.read()
98+
} ?: error("APK missing manifest")
99+
val manifest = BinaryResourceFile(manifestBytes)
100+
val mainChunk = manifest.getMainAxmlChunk()
101+
102+
// Prefetch string indexes to avoid parsing the entire string pool
103+
val networkSecurityConfigStringIdx = mainChunk.stringPool.indexOf("networkSecurityConfig")
104+
val applicationStringIdx = mainChunk.stringPool.indexOf("application")
105+
106+
val applicationChunk = mainChunk.chunks
107+
.find { it is XmlStartElementChunk && it.nameIndex == applicationStringIdx } as? XmlStartElementChunk
108+
?: error("Unable to find <application> in manifest")
109+
val networkSecurityConfig = applicationChunk.attributes
110+
.find { it.nameIndex() == networkSecurityConfigStringIdx }
111+
?: error("Unable to find android:networkSecurityConfig in manifest")
112+
113+
assert(networkSecurityConfig.typedValue().type() == BinaryResourceValue.Type.REFERENCE)
114+
115+
return BinaryResourceIdentifier.create(networkSecurityConfig.typedValue().data())
116+
}
117+
118+
/**
119+
* This generates a binary AXML representation a network security config similar to the one
120+
* manager uses [R.xml.network_security_config], except with resource IDs generated for the patched APK.
121+
*/
122+
private fun generateNetworkConfig(certificateIds: List<BinaryResourceIdentifier>): ByteArray {
123+
val axml = BinaryResourceFile(byteArrayOf())
124+
val xmlChunk = XmlChunk(null)
125+
val strings = StringPoolChunk(xmlChunk)
126+
127+
axml.appendChunk(xmlChunk)
128+
xmlChunk.appendChunk(strings)
129+
xmlChunk.appendChunk(XmlResourceMapChunk(intArrayOf(), xmlChunk))
130+
131+
// Nested chunks
132+
// @formatter:off
133+
val chunkNames = arrayOf("network-security-config", "base-config", "trust-anchors")
134+
for (chunkName in chunkNames) {
135+
xmlChunk.appendChunk(XmlStartElementChunk(
136+
/* namespaceIndex = */ -1,
137+
/* nameIndex = */ strings.addString(chunkName),
138+
/* idIndex = */ -1,
139+
/* classIndex = */ -1,
140+
/* styleIndex = */ -1,
141+
/* attributes = */ emptyList(),
142+
/* parent = */ xmlChunk,
143+
))
144+
}
145+
146+
// Allow "system" certificates
147+
run {
148+
val certificateChunk = XmlStartElementChunk(
149+
/* namespaceIndex = */ -1,
150+
/* nameIndex = */ strings.addString("certificates", /* deduplicate = */ true),
151+
/* idIndex = */ -1,
152+
/* classIndex = */ -1,
153+
/* styleIndex = */ -1,
154+
/* attributes = */ listOf(),
155+
/* parent = */ xmlChunk,
156+
)
157+
certificateChunk.attributes += XmlAttribute(
158+
/* namespaceIndex = */ -1,
159+
/* nameIndex = */ xmlChunk.stringPool.addString("src", /* deduplicate = */ true),
160+
/* rawValueIndex = */ xmlChunk.stringPool.addString("system"),
161+
/* typedValue = */ BinaryResourceValue(
162+
/* type = */ BinaryResourceValue.Type.STRING,
163+
/* data = */ xmlChunk.stringPool.addString("system", /* deduplicate = */ true),
164+
),
165+
/* parent = */ certificateChunk,
166+
)
167+
168+
xmlChunk.appendChunk(certificateChunk)
169+
xmlChunk.appendChunk(XmlEndElementChunk(
170+
/* namespaceIndex = */ -1,
171+
/* nameIndex = */ strings.addString("certificates", /* deduplicate = */ true),
172+
/* parent = */ xmlChunk,
173+
))
174+
}
175+
176+
// Add custom certificate references
177+
for (certificateId in certificateIds) {
178+
val certificateChunk = XmlStartElementChunk(
179+
/* namespaceIndex = */ -1,
180+
/* nameIndex = */ strings.addString("certificates", /* deduplicate = */ true),
181+
/* idIndex = */ -1,
182+
/* classIndex = */ -1,
183+
/* styleIndex = */ -1,
184+
/* attributes = */ listOf(),
185+
/* parent = */ xmlChunk,
186+
)
187+
certificateChunk.attributes += XmlAttribute(
188+
/* namespaceIndex = */ -1,
189+
/* nameIndex = */ xmlChunk.stringPool.addString("src", /* deduplicate = */ true),
190+
/* rawValueIndex = */ -1,
191+
/* typedValue = */ BinaryResourceValue(
192+
/* type = */ BinaryResourceValue.Type.REFERENCE,
193+
/* data = */ certificateId.resourceId(),
194+
),
195+
/* parent = */ certificateChunk,
196+
)
197+
198+
xmlChunk.appendChunk(certificateChunk)
199+
xmlChunk.appendChunk(XmlEndElementChunk(
200+
/* namespaceIndex = */ -1,
201+
/* nameIndex = */ strings.addString("certificates", /* deduplicate = */ true),
202+
/* parent = */ xmlChunk,
203+
))
204+
}
205+
206+
// Reverse nested chunks
207+
for (chunkName in chunkNames.reversed()) {
208+
xmlChunk.appendChunk(XmlEndElementChunk(
209+
/* namespaceIndex = */ -1,
210+
/* nameIndex = */ strings.addString(chunkName, /* deduplicate = */ true),
211+
/* parent = */ xmlChunk,
212+
))
213+
}
214+
// @formatter:on
215+
216+
return axml.toByteArray()
217+
}
218+
219+
private companion object {
220+
val CERTIFICATES = mapOf(
221+
"globalsign_root_r4" to R.raw.globalsign_root_r4,
222+
"gts_root_r1" to R.raw.gts_root_r1,
223+
"gts_root_r2" to R.raw.gts_root_r2,
224+
"gts_root_r3" to R.raw.gts_root_r3,
225+
"gts_root_r4" to R.raw.gts_root_r4,
226+
"isrg_root_x1" to R.raw.isrg_root_x1,
227+
"isrg_root_x2" to R.raw.isrg_root_x2,
228+
)
229+
}
230+
}

app/src/main/kotlin/com/aliucord/manager/patcher/steps/patch/PatchIconsStep.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import com.aliucord.manager.patcher.util.ArscUtil.getResourceFileNames
2828
import com.aliucord.manager.patcher.util.AxmlUtil
2929
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
3030
import com.aliucord.manager.ui.screens.patchopts.PatchOptions.IconReplacement
31-
import com.aliucord.manager.util.getResBytes
31+
import com.aliucord.manager.util.getRawBytes
3232
import com.github.diamondminer88.zip.ZipWriter
3333
import com.google.devrel.gmscore.tools.apk.arsc.*
3434
import org.koin.core.component.KoinComponent
@@ -184,12 +184,12 @@ class PatchIconsStep(private val options: PatchOptions) : Step(), KoinComponent
184184
}
185185

186186
container.log("Writing monochrome icon AXML to apk")
187-
it.writeEntry("res/ic_aliucord_monochrome.xml", context.getResBytes(monochromeIconId))
187+
it.writeEntry("res/ic_aliucord_monochrome.xml", context.resources.getRawBytes(monochromeIconId))
188188
}
189189

190190
if (options.iconReplacement is IconReplacement.OldDiscord) {
191191
container.log("Writing custom icon foreground to apk")
192-
it.writeEntry("res/ic_foreground_replacement.xml", context.getResBytes(R.drawable.ic_discord_old_monochrome))
192+
it.writeEntry("res/ic_foreground_replacement.xml", context.resources.getRawBytes(R.drawable.ic_discord_old_monochrome))
193193
} else if (options.iconReplacement is IconReplacement.CustomImage) {
194194
container.log("Writing custom icon foreground to apk")
195195
it.writeEntry("res/ic_foreground_replacement.png", options.iconReplacement.imageBytes)

app/src/main/kotlin/com/aliucord/manager/patcher/util/AxmlUtil.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ object AxmlUtil {
2727
/**
2828
* Get the only top-level chunk in an axml file.
2929
*/
30-
private fun BinaryResourceFile.getMainAxmlChunk(): XmlChunk {
30+
fun BinaryResourceFile.getMainAxmlChunk(): XmlChunk {
3131
if (this.chunks.size > 1)
3232
error("More than 1 top level chunk in axml")
3333

@@ -163,8 +163,9 @@ object AxmlUtil {
163163
* This is then used to get the filename of the resource from `resources.arsc`.
164164
*/
165165
fun readManifestIconInfo(apk: File): ManifestIconInfo {
166-
val manifestBytes = ZipReader(apk).use { it.openEntry("AndroidManifest.xml")?.read() }
167-
?: error("APK missing manifest")
166+
val manifestBytes = ZipReader(apk).use {
167+
it.openEntry("AndroidManifest.xml")?.read()
168+
} ?: error("APK missing manifest")
168169
val manifest = BinaryResourceFile(manifestBytes)
169170
val mainChunk = manifest.getMainAxmlChunk()
170171

app/src/main/kotlin/com/aliucord/manager/patcher/util/ManifestPatcher.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ object ManifestPatcher {
1616
private const val USE_EMBEDDED_DEX = "useEmbeddedDex"
1717
private const val EXTRACT_NATIVE_LIBS = "extractNativeLibs"
1818
private const val REQUEST_LEGACY_EXTERNAL_STORAGE = "requestLegacyExternalStorage"
19-
private const val NETWORK_SECURITY_CONFIG = "networkSecurityConfig"
2019
private const val LABEL = "label"
2120
private const val PACKAGE = "package"
2221
private const val COMPILE_SDK_VERSION = "compileSdkVersion"
@@ -128,7 +127,6 @@ object ManifestPatcher {
128127
private var addMetadata = true
129128

130129
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
131-
if (name == NETWORK_SECURITY_CONFIG) return
132130
if (name == REQUEST_LEGACY_EXTERNAL_STORAGE) addLegacyStorage = false
133131
if (name == USE_EMBEDDED_DEX) addUseEmbeddedDex = false
134132
if (name == EXTRACT_NATIVE_LIBS) addExtractNativeLibs = false

app/src/main/kotlin/com/aliucord/manager/util/Context.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
44
import android.app.Activity
55
import android.content.*
66
import android.content.pm.PackageManager
7+
import android.content.res.Resources
78
import android.net.ConnectivityManager
89
import android.net.Uri
910
import android.os.*
@@ -16,8 +17,7 @@ import androidx.annotation.AnyRes
1617
import androidx.annotation.StringRes
1718
import androidx.core.content.ContextCompat
1819
import androidx.core.content.getSystemService
19-
import com.aliucord.manager.BuildConfig
20-
import com.aliucord.manager.R
20+
import com.aliucord.manager.*
2121
import com.google.android.gms.safetynet.SafetyNet
2222
import java.io.File
2323
import java.io.InputStream
@@ -111,21 +111,21 @@ fun Activity.requestNoBatteryOptimizations() {
111111
}
112112

113113
/**
114-
* Get the raw bytes for a resource.
114+
* Get the raw bytes for any resource stored as a file within the APK.
115115
* @param id The resource identifier
116-
* @return The resource's raw bytes as stored inside the APK
116+
* @return The resource's raw bytes as stored inside the APK (no parsing is done).
117117
*/
118-
fun Context.getResBytes(@AnyRes id: Int): ByteArray {
118+
fun Resources.getRawBytes(@AnyRes id: Int): ByteArray {
119119
val tValue = TypedValue()
120-
this.resources.getValue(
120+
this.getValue(
121121
/* id = */ id,
122122
/* outValue = */ tValue,
123123
/* resolveRefs = */ true,
124124
)
125125

126126
val resPath = tValue.string.toString()
127127

128-
return this.javaClass.classLoader
128+
return ManagerApplication::class.java.classLoader
129129
?.getResourceAsStream(resPath)
130130
?.use(InputStream::readBytes)
131131
?: error("Failed to get resource file $resPath from APK")

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
<string name="patch_step_dl_smali">Downloading smali patches</string>
163163
<string name="patch_step_copy_deps">Copying dependencies</string>
164164
<string name="patch_step_patch_manifests">Patching APK manifest</string>
165+
<string name="patch_step_patch_certs">Adding modern root certificates</string>
165166
<string name="patch_step_patch_icon">Patching app icon</string>
166167
<string name="patch_step_add_aliuhook">Adding Aliuhook library</string>
167168
<string name="patch_step_patch_smali">Applying smali patches</string>

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ androidx-lifecycle = "2.9.4"
77
androidx-splashscreen = "1.0.1"
88
apksig = "8.13.0"
99
axml = "1.0.1"
10-
binary-resources = "2.0.1"
10+
binary-resources = "2.1.0"
1111
bouncycastle = "1.82"
1212
coil = "3.3.0"
1313
compose = "1.9.4"

0 commit comments

Comments
 (0)