release: harden Android network security
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.yincheng.wygj">
|
||||
<application
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="false"
|
||||
tools:replace="android:networkSecurityConfig,android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "aihr-mobile-uni",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aihr-mobile-uni",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-alpha-5020220260725001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-alpha-5020220260725001",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package",
|
||||
"name": "aihr-mobile-uni",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "uni -p h5",
|
||||
"dev:h5": "uni -p h5",
|
||||
"build:h5": "uni build -p h5",
|
||||
"build:app": "uni build -p app",
|
||||
"build:app": "uni build -p app && node scripts/sync-app-native-resources.mjs",
|
||||
"generate:app-assets": "sh scripts/generate-app-assets.sh",
|
||||
"configure:app-privacy": "node scripts/configure-android-privacy.mjs",
|
||||
"verify:app-assets": "node scripts/verify-app-assets.mjs",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cpSync, existsSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const projectRoot = resolve(import.meta.dirname, '..');
|
||||
const mappings = [
|
||||
[
|
||||
resolve(projectRoot, 'AndroidManifest.xml'),
|
||||
resolve(projectRoot, 'dist/build/app/AndroidManifest.xml')
|
||||
],
|
||||
[
|
||||
resolve(projectRoot, 'nativeResources/android/res/xml/network_security_config.xml'),
|
||||
resolve(projectRoot, 'dist/build/app/nativeResources/android/res/xml/network_security_config.xml')
|
||||
]
|
||||
];
|
||||
|
||||
for (const [source, destination] of mappings) {
|
||||
if (!existsSync(source)) {
|
||||
throw new Error(`Missing required native App resource: ${source}`);
|
||||
}
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
cpSync(source, destination);
|
||||
}
|
||||
|
||||
console.log('Android native manifest and network security resources synchronized.');
|
||||
@@ -205,19 +205,45 @@ try {
|
||||
$application = $decodedManifest.manifest.application
|
||||
$debuggable = $application.GetAttribute('debuggable', $androidNamespace) -eq 'true'
|
||||
$usesCleartextTraffic = $application.GetAttribute('usesCleartextTraffic', $androidNamespace) -eq 'true'
|
||||
$networkSecurityConfig = $application.GetAttribute('networkSecurityConfig', $androidNamespace)
|
||||
if ($BuildKind -eq 'custom-base' -and -not $debuggable) {
|
||||
throw 'Custom base APK must remain debuggable.'
|
||||
}
|
||||
if ($BuildKind -eq 'dcloud-test' -and $debuggable) {
|
||||
throw 'DCloud internal test APK must not be debuggable.'
|
||||
}
|
||||
if ($BuildKind -in @('dcloud-test', 'release') -and $usesCleartextTraffic) {
|
||||
throw "$BuildKind APK must not allow cleartext HTTP traffic."
|
||||
}
|
||||
if ($BuildKind -in @('dcloud-test', 'release')) {
|
||||
if ($networkSecurityConfig -ne '@xml/network_security_config') {
|
||||
throw "$BuildKind APK must reference @xml/network_security_config."
|
||||
}
|
||||
$decodedNetworkSecurityPath = Join-Path $decodePath 'res\xml\network_security_config.xml'
|
||||
if (-not (Test-Path -LiteralPath $decodedNetworkSecurityPath)) {
|
||||
throw "$BuildKind APK is missing res/xml/network_security_config.xml."
|
||||
}
|
||||
$decodedNetworkSecurity = Get-Content -Raw -Encoding UTF8 -LiteralPath $decodedNetworkSecurityPath
|
||||
if ($decodedNetworkSecurity -notmatch 'cleartextTrafficPermitted=\"false\"') {
|
||||
throw "$BuildKind APK network security config must reject cleartext traffic."
|
||||
}
|
||||
if ($decodedNetworkSecurity -match 'cleartextTrafficPermitted=\"true\"') {
|
||||
throw "$BuildKind APK network security config must not contain a cleartext exception."
|
||||
}
|
||||
if ($decodedNetworkSecurity -notmatch '<certificates src=\"system\"\s*/>') {
|
||||
throw "$BuildKind APK network security config must trust system certificates only."
|
||||
}
|
||||
if ($decodedNetworkSecurity -match '<certificates src=\"(?!system\")[^\"]+\"') {
|
||||
throw "$BuildKind APK network security config must not trust user or bundled certificates."
|
||||
}
|
||||
if ($decodedNetworkSecurity -match '<debug-overrides\b') {
|
||||
throw "$BuildKind APK network security config must not contain debug trust overrides."
|
||||
}
|
||||
}
|
||||
if ($BuildKind -eq 'release') {
|
||||
if ($debuggable) {
|
||||
throw 'Release APK must not be debuggable.'
|
||||
}
|
||||
if ($usesCleartextTraffic) {
|
||||
throw 'Release APK must not allow cleartext HTTP traffic.'
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($ExpectedSignerSha256)) {
|
||||
throw 'Release verification requires -ExpectedSignerSha256 for the enterprise signing certificate.'
|
||||
}
|
||||
|
||||
@@ -15,6 +15,16 @@ let releaseLegalUrls;
|
||||
|
||||
const fail = (message) => failures.push(message);
|
||||
const compiledAppRoot = resolve(projectRoot, 'dist/build/app');
|
||||
const sourceAndroidManifestPath = resolve(projectRoot, 'AndroidManifest.xml');
|
||||
const compiledAndroidManifestPath = resolve(compiledAppRoot, 'AndroidManifest.xml');
|
||||
const sourceNetworkSecurityPath = resolve(
|
||||
projectRoot,
|
||||
'nativeResources/android/res/xml/network_security_config.xml'
|
||||
);
|
||||
const compiledNetworkSecurityPath = resolve(
|
||||
compiledAppRoot,
|
||||
'nativeResources/android/res/xml/network_security_config.xml'
|
||||
);
|
||||
const releaseTextExtensions = new Set(['.css', '.html', '.js', '.json', '.txt', '.xml']);
|
||||
const releaseSensitiveFilePattern = /(^|[\\/])(?:\.env(?:\.|$)|id_rsa$)|\.(?:jks|keystore|p12|pfx|pem)$/i;
|
||||
const releaseSensitiveContentPatterns = [
|
||||
@@ -126,7 +136,7 @@ if (app?.ssl?.untrustedca !== 'refuse') {
|
||||
fail('app-plus.ssl.untrustedca must remain refuse');
|
||||
}
|
||||
if (!/^\d+\.\d+\.\d+$/.test(String(manifest.versionName || ''))) {
|
||||
fail('versionName must use semantic numeric form such as 0.1.6');
|
||||
fail('versionName must use semantic numeric form such as 0.1.8');
|
||||
}
|
||||
if (!/^[1-9]\d*$/.test(String(manifest.versionCode || ''))) {
|
||||
fail('versionCode must be a positive integer');
|
||||
@@ -137,6 +147,9 @@ if (android?.packagename !== 'com.yincheng.wygj') {
|
||||
if (android?.targetSdkVersion !== 35) {
|
||||
fail('Android targetSdkVersion must be 35 for the August 1 test package');
|
||||
}
|
||||
if (android?.usesCleartextTraffic !== false) {
|
||||
fail('Android manifest configuration must explicitly disable cleartext traffic');
|
||||
}
|
||||
const expectedAndroidAbis = ['arm64-v8a', 'armeabi-v7a'];
|
||||
const actualAndroidAbis = Array.isArray(android?.abiFilters)
|
||||
? [...android.abiFilters].sort()
|
||||
@@ -162,6 +175,46 @@ for (const permission of ['android.permission.RECORD_AUDIO', 'android.permission
|
||||
}
|
||||
}
|
||||
|
||||
const verifyAndroidNetworkSecurity = (manifestPath, networkSecurityPath, label) => {
|
||||
if (!existsSync(manifestPath)) {
|
||||
fail(`${label} is missing AndroidManifest.xml`);
|
||||
return;
|
||||
}
|
||||
if (!existsSync(networkSecurityPath)) {
|
||||
fail(`${label} is missing network_security_config.xml`);
|
||||
return;
|
||||
}
|
||||
const androidManifest = readFileSync(manifestPath, 'utf8');
|
||||
const networkSecurity = readFileSync(networkSecurityPath, 'utf8');
|
||||
if (!/android:usesCleartextTraffic\s*=\s*["']false["']/.test(androidManifest)) {
|
||||
fail(`${label} AndroidManifest.xml must disable cleartext traffic`);
|
||||
}
|
||||
if (!/android:networkSecurityConfig\s*=\s*["']@xml\/network_security_config["']/.test(androidManifest)) {
|
||||
fail(`${label} AndroidManifest.xml must reference network_security_config`);
|
||||
}
|
||||
if (!/cleartextTrafficPermitted\s*=\s*["']false["']/.test(networkSecurity)) {
|
||||
fail(`${label} network security config must reject cleartext traffic`);
|
||||
}
|
||||
if (/cleartextTrafficPermitted\s*=\s*["']true["']/.test(networkSecurity)) {
|
||||
fail(`${label} network security config must not contain a cleartext exception`);
|
||||
}
|
||||
if (!/<certificates\s+src\s*=\s*["']system["']\s*\/>/.test(networkSecurity)) {
|
||||
fail(`${label} network security config must trust system certificates only`);
|
||||
}
|
||||
if (/<certificates\s+src\s*=\s*["'](?!system["'])[^"']+["']/.test(networkSecurity)) {
|
||||
fail(`${label} network security config must not trust user or bundled certificates`);
|
||||
}
|
||||
if (/<debug-overrides\b/.test(networkSecurity)) {
|
||||
fail(`${label} network security config must not contain debug trust overrides`);
|
||||
}
|
||||
};
|
||||
|
||||
verifyAndroidNetworkSecurity(
|
||||
sourceAndroidManifestPath,
|
||||
sourceNetworkSecurityPath,
|
||||
'source App'
|
||||
);
|
||||
|
||||
const androidIcons = [
|
||||
['hdpi', 72], ['xhdpi', 96], ['xxhdpi', 144], ['xxxhdpi', 192]
|
||||
];
|
||||
@@ -251,6 +304,11 @@ if (requirePrivacy) {
|
||||
fail(`compiled App androidPrivacy.json is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
verifyAndroidNetworkSecurity(
|
||||
compiledAndroidManifestPath,
|
||||
compiledNetworkSecurityPath,
|
||||
'compiled App'
|
||||
);
|
||||
scanCompiledAppForSensitiveValues();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"appid" : "__UNI__B36D8BE",
|
||||
"description" : "帮道员工端",
|
||||
"vueVersion" : "3",
|
||||
"versionName" : "0.1.6",
|
||||
"versionCode" : "106",
|
||||
"versionName" : "0.1.8",
|
||||
"versionCode" : "108",
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
@@ -89,6 +89,7 @@
|
||||
"android" : {
|
||||
"packagename" : "com.yincheng.wygj",
|
||||
"targetSdkVersion" : 35,
|
||||
"usesCleartextTraffic" : false,
|
||||
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ],
|
||||
"permissionExternalStorage" : {
|
||||
"request" : "none"
|
||||
|
||||
@@ -42,6 +42,38 @@ test('Android 自定义基座显式面向 Android 15 权限模型', async () =>
|
||||
|
||||
assert.equal(android?.packagename, 'com.yincheng.wygj');
|
||||
assert.equal(android?.targetSdkVersion, 35, '不得回退到 DCloud 默认的 targetSdkVersion 28 兼容模式');
|
||||
assert.equal(android?.usesCleartextTraffic, false, '发布配置不得允许明文 HTTP');
|
||||
});
|
||||
|
||||
test('Android 原生网络安全配置拒绝明文流量且只信任系统证书', async () => {
|
||||
const androidManifest = await readFile(
|
||||
new URL('../AndroidManifest.xml', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const networkSecurity = await readFile(
|
||||
new URL('../nativeResources/android/res/xml/network_security_config.xml', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(androidManifest, /android:usesCleartextTraffic="false"/);
|
||||
assert.match(androidManifest, /android:networkSecurityConfig="@xml\/network_security_config"/);
|
||||
assert.match(networkSecurity, /cleartextTrafficPermitted="false"/);
|
||||
assert.match(networkSecurity, /<certificates src="system" \/>/);
|
||||
assert.doesNotMatch(networkSecurity, /cleartextTrafficPermitted="true"/);
|
||||
assert.doesNotMatch(networkSecurity, /<certificates src="user"/);
|
||||
assert.doesNotMatch(networkSecurity, /<debug-overrides\b/);
|
||||
});
|
||||
|
||||
test('App 构建链将 Android 原生网络安全资源同步到云打包输入目录', async () => {
|
||||
const pkg = await readJson('../package.json');
|
||||
const syncScript = await readFile(
|
||||
new URL('../scripts/sync-app-native-resources.mjs', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
assert.match(pkg.scripts?.['build:app'] || '', /sync-app-native-resources\.mjs/);
|
||||
assert.match(syncScript, /dist\/build\/app\/AndroidManifest\.xml/);
|
||||
assert.match(syncScript, /dist\/build\/app\/nativeResources\/android\/res\/xml\/network_security_config\.xml/);
|
||||
});
|
||||
|
||||
test('Android 启动阶段不主动索取设备信息或外部存储权限', async () => {
|
||||
@@ -146,6 +178,12 @@ test('APK 门禁区分自定义调试基座与内置业务资源的测试包', a
|
||||
assert.match(verifier, /dist\\build\\app-plus\\app-service\.js/);
|
||||
assert.match(verifier, /fixed test SMS code/);
|
||||
assert.match(verifier, /'mobile number'\s*=/);
|
||||
assert.match(verifier, /dcloud-test.*release.*usesCleartextTraffic/s);
|
||||
assert.match(verifier, /res\\xml\\network_security_config\.xml/);
|
||||
assert.match(verifier, /network security config must trust system certificates only/);
|
||||
assert.match(verifier, /must not contain a cleartext exception/);
|
||||
assert.match(verifier, /must not trust user or bundled certificates/);
|
||||
assert.match(verifier, /must not contain debug trust overrides/);
|
||||
});
|
||||
|
||||
test('8 月 1 日 RC 脚本默认拒绝脏工作区且记录完整构建证据', async () => {
|
||||
|
||||
Reference in New Issue
Block a user