Skip to content

Commit 36aea58

Browse files
fix(mobile): trust user-installed Android CA certificates (#25730)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7c6446b commit 36aea58

4 files changed

Lines changed: 61 additions & 19 deletions

File tree

‎apps/mobile/app.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ const config: ExpoConfig = {
425425
["expo-sensors", { motionPermission: false }],
426426
// Mods run in reverse order; expo-widgets must create and wipe the target before assets are added.
427427
...(!isIosPersonalTeamBuild ? ["./plugins/withWidgetLogoAsset.cjs", widgetsPlugin] : []),
428-
"./plugins/withAndroidCleartextTraffic.cjs",
428+
"./plugins/withAndroidNetworkSecurityConfig.cjs",
429429
"./plugins/withAndroidGradleHeap.cjs",
430430
"./plugins/withAndroidInputBackground.cjs",
431431
"./plugins/withAndroidModernPopupMenu.cjs",

‎apps/mobile/plugins/withAndroidCleartextTraffic.cjs‎

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const fs = require("node:fs");
2+
const path = require("node:path");
3+
const { withAndroidManifest, withDangerousMod } = require("expo/config-plugins");
4+
5+
// Environments behind a private CA installed in the user store must work like they do in Chrome.
6+
// Android ignores usesCleartextTraffic once a network security config exists, so permit it here.
7+
const NETWORK_SECURITY_CONFIG = `<?xml version="1.0" encoding="utf-8"?>
8+
<network-security-config>
9+
<base-config cleartextTrafficPermitted="true">
10+
<trust-anchors>
11+
<certificates src="system" />
12+
<certificates src="user" />
13+
</trust-anchors>
14+
</base-config>
15+
</network-security-config>
16+
`;
17+
18+
function withNetworkSecurityConfigManifest(config) {
19+
return withAndroidManifest(config, (nextConfig) => {
20+
const application = nextConfig.modResults.manifest.application?.[0];
21+
22+
if (application == null) {
23+
throw new Error(
24+
"AndroidManifest.xml is missing the application element required for network security configuration.",
25+
);
26+
}
27+
28+
application.$ ??= {};
29+
application.$["android:networkSecurityConfig"] = "@xml/network_security_config";
30+
31+
return nextConfig;
32+
});
33+
}
34+
35+
function withNetworkSecurityConfigResource(config) {
36+
return withDangerousMod(config, [
37+
"android",
38+
async (config) => {
39+
const xmlDir = path.join(
40+
config.modRequest.platformProjectRoot,
41+
"app",
42+
"src",
43+
"main",
44+
"res",
45+
"xml",
46+
);
47+
fs.mkdirSync(xmlDir, { recursive: true });
48+
fs.writeFileSync(path.join(xmlDir, "network_security_config.xml"), NETWORK_SECURITY_CONFIG);
49+
return config;
50+
},
51+
]);
52+
}
53+
54+
module.exports = function withAndroidNetworkSecurityConfig(config) {
55+
return withNetworkSecurityConfigResource(withNetworkSecurityConfigManifest(config));
56+
};

‎docs/user/remote-access.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ On a computer with the desktop app, you can open the printed `t3code://pair`
6262
app link instead. The desktop app asks you to confirm the server address, then
6363
adds it.
6464

65+
If your HTTPS endpoint uses a certificate from your own certificate authority,
66+
install that CA on Android as a **CA certificate** in its security settings; the
67+
Android app then trusts it.
68+
6569
Pairing authorizes that device for future connections. Use a fresh one-time link
6670
for each new device; you do not need the original token to reconnect. Links
6771
created in Settings can only be copied from the client that created them while

0 commit comments

Comments
 (0)