release: harden Android network security
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user