import { createHash, randomBytes } from 'node:crypto' import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readSync, readdirSync, realpathSync, rmSync, writeFileSync, } from 'node:fs' import { tmpdir } from 'node:os' import { basename, join, resolve, sep } from 'node:path' import { spawnSync } from 'node:child_process' import { inflateRawSync } from 'node:zlib' const EXPECTED_PACKAGE = 'com.d3ro.voice' const EXPECTED_MIN_SDK = 24 const EXPECTED_TARGET_SDK = 36 const MODEL_PATH = 'assets/models/ggml-tiny.bin' const MODEL_BYTES = 77_691_713 const MODEL_SHA256 = 'be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21' const TEST_ADMOB_APP_ID = 'ca-app-pub-3940256099942544~3347511713' const COMPROMISED_SIGNER_SHA256 = '06eec757722ee7cd3dfbc53202d974aaf0417a7d397f9ef1e5611088ebb2e481' const DEBUG_PROPS_SYMBOL = Buffer.from('_ZNK8facebook5react5Props13getDebugPropsEv') const ANDROID_16_KIB_PAGE_BYTES = 16 * 1024 const ELF_PT_LOAD = 1 const VALID_MODES = new Set(['debug', 'e2e', 'release']) const REQUIRED_NATIVE_LIBRARIES = [ 'libappmodules.so', 'libc++_shared.so', 'libfbjni.so', 'libgesturehandler.so', 'libhermesvm.so', 'libjsi.so', 'libNitroIap.so', 'libNitroModules.so', 'libNitroSound.so', 'libreact_codegen_rnscreens.so', 'libreact_codegen_safeareacontext.so', 'libreactnative.so', 'librnscreens.so', 'librnwhisper.so', ] function fail(message) { throw new Error(`android_artifact_invalid:${message}`) } function normalizeCertificateSha256(value, label) { if (typeof value !== 'string') fail(`${label}_missing`) const normalized = value.replaceAll(':', '').trim().toLowerCase() if (!/^[0-9a-f]{64}$/.test(normalized)) fail(`${label}_format`) return normalized } function requireAllowedReleaseCertificate(value, label) { const normalized = normalizeCertificateSha256(value, label) if (normalized === COMPROMISED_SIGNER_SHA256) fail(`${label}_compromised`) return normalized } function stableStatValue(stat, key, fallbackKey) { if (key in stat) return stat[key] return BigInt(Math.trunc(Number(stat[fallbackKey]) * 1_000_000)) } function sameFileSnapshot(left, right) { return (process.platform === 'win32' || left.dev === right.dev) && left.ino === right.ino && left.size === right.size && stableStatValue(left, 'mtimeNs', 'mtimeMs') === stableStatValue(right, 'mtimeNs', 'mtimeMs') && stableStatValue(left, 'ctimeNs', 'ctimeMs') === stableStatValue(right, 'ctimeNs', 'ctimeMs') } function parseArguments(argv) { const allowed = new Set([ 'aab', 'apk', 'bundletool', 'expected-admob-app-id', 'expected-upload-cert-sha256', 'expected-version-code', 'expected-version-name', 'mode', ]) const options = {} for (let index = 0; index < argv.length; index += 1) { const current = argv[index] if (!current.startsWith('--')) fail(`unexpected_argument_${current}`) const value = argv[index + 1] if (!value || value.startsWith('--')) fail(`missing_value_${current.slice(2)}`) const name = current.slice(2) if (!allowed.has(name) || Object.hasOwn(options, name)) fail(`unexpected_argument_${name}`) options[name] = value index += 1 } return options } function readExactly(fd, length, position) { const buffer = Buffer.alloc(length) let offset = 0 while (offset < length) { const bytesRead = readSync(fd, buffer, offset, length - offset, position + offset) if (bytesRead === 0) fail('unexpected_end_of_zip') offset += bytesRead } return buffer } function readZipDirectory(path) { const absolute = resolve(path) const pathStat = lstatSync(absolute, { bigint: true }) if (pathStat.isSymbolicLink()) fail(`artifact_symlink_${basename(absolute)}`) if (realpathSync(absolute) !== absolute) fail(`artifact_reparse_${basename(absolute)}`) if (!pathStat.isFile()) fail(`artifact_not_regular_${basename(absolute)}`) if (pathStat.nlink !== 1n) fail(`artifact_hardlink_${basename(absolute)}`) const fd = openSync(absolute, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0)) try { const openedStat = fstatSync(fd, { bigint: true }) if (!sameFileSnapshot(pathStat, openedStat)) fail(`artifact_identity_${basename(absolute)}`) const size = Number(openedStat.size) const tailLength = Math.min(size, 65_557) const tail = readExactly(fd, tailLength, size - tailLength) let eocd = -1 for (let index = tail.length - 22; index >= 0; index -= 1) { if (tail.readUInt32LE(index) === 0x06054b50) { eocd = index break } } if (eocd < 0) fail('zip_eocd_missing') const entriesCount = tail.readUInt16LE(eocd + 10) const directorySize = tail.readUInt32LE(eocd + 12) const directoryOffset = tail.readUInt32LE(eocd + 16) if (entriesCount === 0xffff || directoryOffset === 0xffffffff) fail('zip64_not_supported') const directory = readExactly(fd, directorySize, directoryOffset) const entries = new Map() let cursor = 0 for (let index = 0; index < entriesCount; index += 1) { if (directory.readUInt32LE(cursor) !== 0x02014b50) fail('central_directory_corrupt') const method = directory.readUInt16LE(cursor + 10) const compressedSize = directory.readUInt32LE(cursor + 20) const uncompressedSize = directory.readUInt32LE(cursor + 24) const nameLength = directory.readUInt16LE(cursor + 28) const extraLength = directory.readUInt16LE(cursor + 30) const commentLength = directory.readUInt16LE(cursor + 32) const localOffset = directory.readUInt32LE(cursor + 42) const nameStart = cursor + 46 const name = directory.subarray(nameStart, nameStart + nameLength).toString('utf8') if (entries.has(name)) fail(`duplicate_zip_entry_${name}`) entries.set(name, { method, compressedSize, uncompressedSize, localOffset }) cursor = nameStart + nameLength + extraLength + commentLength } return { absolute, fd, entries, keepOpen: true, openedStat } } catch (error) { closeSync(fd) throw error } } function assertArchivePathStable(archive, label) { const current = lstatSync(archive.absolute, { bigint: true }) if (current.isSymbolicLink() || !current.isFile() || current.nlink !== 1n) { fail(`${label}_path_not_sealed`) } const opened = fstatSync(archive.fd, { bigint: true }) if (!sameFileSnapshot(archive.openedStat, current) || !sameFileSnapshot(archive.openedStat, opened)) { fail(`${label}_path_changed`) } } function hashArchiveStable(archive, label) { assertArchivePathStable(archive, label) const hash = createHash('sha256') const chunk = Buffer.allocUnsafe(1024 * 1024) let position = 0 while (position < Number(archive.openedStat.size)) { const count = readSync(archive.fd, chunk, 0, chunk.length, position) if (count === 0) fail(`${label}_hash_short_read`) hash.update(chunk.subarray(0, count)) position += count } assertArchivePathStable(archive, label) return hash.digest('hex') } function extractEntry(archive, name) { const entry = archive.entries.get(name) if (!entry) fail(`missing_entry_${name}`) const header = readExactly(archive.fd, 30, entry.localOffset) if (header.readUInt32LE(0) !== 0x04034b50) fail(`local_header_corrupt_${name}`) const nameLength = header.readUInt16LE(26) const extraLength = header.readUInt16LE(28) const dataOffset = entry.localOffset + 30 + nameLength + extraLength const compressed = readExactly(archive.fd, entry.compressedSize, dataOffset) const output = entry.method === 0 ? compressed : entry.method === 8 ? inflateRawSync(compressed) : fail(`unsupported_zip_method_${entry.method}_${name}`) if (output.length !== entry.uncompressedSize) fail(`entry_size_mismatch_${name}`) return output } function nativeEntryFailureLabel(name) { return name.replace(/[^a-z0-9_.-]+/gi, '_') } function readElfInteger(buffer, offset, byteLength, label) { if (!Number.isSafeInteger(offset) || offset < 0 || offset + byteLength > buffer.length) { fail(`${label}_elf_truncated`) } if (byteLength === 2) { return BigInt(buffer.readUInt16LE(offset)) } if (byteLength === 4) { return BigInt(buffer.readUInt32LE(offset)) } if (byteLength === 8) { return buffer.readBigUInt64LE(offset) } fail(`${label}_elf_integer_width_${byteLength}`) } function verifyElfLoadSegmentAlignment(buffer, label) { if (!Buffer.isBuffer(buffer) || buffer.length < 16) fail(`${label}_elf_header_truncated`) if ( buffer[0] !== 0x7f || buffer[1] !== 0x45 || buffer[2] !== 0x4c || buffer[3] !== 0x46 ) { fail(`${label}_elf_magic`) } const elfClass = buffer[4] const dataEncoding = buffer[5] if (elfClass !== 2) fail(`${label}_elf_class_${elfClass}`) if (dataEncoding !== 1) fail(`${label}_elf_data_encoding_${dataEncoding}`) if (buffer[6] !== 1) fail(`${label}_elf_ident_version_${buffer[6]}`) const headerBytes = 64 const minimumProgramHeaderBytes = 56 if (buffer.length < headerBytes) fail(`${label}_elf_header_truncated`) const type = readElfInteger(buffer, 16, 2, label) if (type !== 3n) fail(`${label}_elf_type_${type}`) const machine = readElfInteger(buffer, 18, 2, label) if (machine !== 183n) fail(`${label}_elf_machine_${machine}`) const elfVersion = readElfInteger(buffer, 20, 4, label) if (elfVersion !== 1n) fail(`${label}_elf_version_${elfVersion}`) const programHeaderOffset = readElfInteger(buffer, 32, 8, label) const programHeaderEntryBytes = Number(readElfInteger(buffer, 54, 2, label)) const programHeaderCount = Number(readElfInteger(buffer, 56, 2, label)) if (programHeaderCount === 0xffff) fail(`${label}_elf_extended_program_header_count_unverifiable`) if (programHeaderCount === 0) fail(`${label}_elf_program_headers_missing`) if (programHeaderEntryBytes < minimumProgramHeaderBytes) { fail(`${label}_elf_program_header_size_${programHeaderEntryBytes}`) } const programHeaderTableEnd = programHeaderOffset + BigInt(programHeaderEntryBytes) * BigInt(programHeaderCount) if ( programHeaderOffset > BigInt(Number.MAX_SAFE_INTEGER) || programHeaderTableEnd > BigInt(buffer.length) ) { fail(`${label}_elf_program_header_table_truncated`) } let loadSegmentCount = 0 let minimumLoadAlignment = null for (let index = 0; index < programHeaderCount; index += 1) { const headerOffset = Number(programHeaderOffset) + index * programHeaderEntryBytes const type = Number(readElfInteger(buffer, headerOffset, 4, label)) if (type !== ELF_PT_LOAD) continue const fileOffset = readElfInteger(buffer, headerOffset + 8, 8, label) const virtualAddress = readElfInteger( buffer, headerOffset + 16, 8, label, ) const fileBytes = readElfInteger(buffer, headerOffset + 32, 8, label) const memoryBytes = readElfInteger(buffer, headerOffset + 40, 8, label) const alignment = readElfInteger(buffer, headerOffset + 48, 8, label) if (fileOffset + fileBytes > BigInt(buffer.length)) { fail(`${label}_elf_load_${index}_file_range`) } if (memoryBytes < fileBytes) fail(`${label}_elf_load_${index}_memory_range`) if (alignment < BigInt(ANDROID_16_KIB_PAGE_BYTES)) { fail(`${label}_elf_load_${index}_alignment_${alignment}`) } if ((alignment & (alignment - 1n)) !== 0n) { fail(`${label}_elf_load_${index}_alignment_not_power_of_two_${alignment}`) } if (fileOffset % alignment !== virtualAddress % alignment) { fail(`${label}_elf_load_${index}_offset_address_misaligned`) } minimumLoadAlignment = minimumLoadAlignment === null || alignment < minimumLoadAlignment ? alignment : minimumLoadAlignment loadSegmentCount += 1 } if (loadSegmentCount === 0) fail(`${label}_elf_load_segments_missing`) return { loadSegmentCount, minimumLoadAlignmentBytes: minimumLoadAlignment.toString(), } } function listNativeLibraryNames(archive, matchesName) { return [...archive.entries.keys()].filter(matchesName).sort() } function verifyArchiveNativeElfAlignment(archive, matchesName, label) { const nativeNames = listNativeLibraryNames(archive, matchesName) if (nativeNames.length === 0) fail(`${label}_native_libraries_missing`) let loadSegmentCount = 0 let minimumLoadAlignment = null const librarySha256s = [] for (const name of nativeNames) { const entryLabel = `${label}_${nativeEntryFailureLabel(name)}` const library = extractEntry(archive, name) const evidence = verifyElfLoadSegmentAlignment(library, entryLabel) librarySha256s.push(createHash('sha256').update(library).digest('hex')) const alignment = BigInt(evidence.minimumLoadAlignmentBytes) minimumLoadAlignment = minimumLoadAlignment === null || alignment < minimumLoadAlignment ? alignment : minimumLoadAlignment loadSegmentCount += evidence.loadSegmentCount } return { evidence: { requiredPageSizeBytes: ANDROID_16_KIB_PAGE_BYTES, elfLibrariesChecked: nativeNames.length, elfLoadSegmentsChecked: loadSegmentCount, minimumElfLoadAlignmentBytes: minimumLoadAlignment.toString(), }, librarySha256s, } } function readLocalEntryMetadata(archive, name) { const entry = archive.entries.get(name) if (!entry) fail(`missing_entry_${name}`) const header = readExactly(archive.fd, 30, entry.localOffset) if (header.readUInt32LE(0) !== 0x04034b50) fail(`local_header_corrupt_${name}`) const flags = header.readUInt16LE(6) const method = header.readUInt16LE(8) const nameLength = header.readUInt16LE(26) const extraLength = header.readUInt16LE(28) const localName = readExactly(archive.fd, nameLength, entry.localOffset + 30).toString('utf8') if (localName !== name) fail(`local_name_mismatch_${name}`) if (method !== entry.method) fail(`local_method_mismatch_${name}`) const dataOffset = entry.localOffset + 30 + nameLength + extraLength if (BigInt(dataOffset + entry.compressedSize) > archive.openedStat.size) { fail(`local_data_range_${name}`) } return { dataOffset, flags, method } } function requireStoredNativeZipAlignment(method, dataOffset, label) { if (method === 8) return false if (method !== 0) fail(`${label}_zip_method_${method}_unverifiable`) if (dataOffset % ANDROID_16_KIB_PAGE_BYTES !== 0) { fail(`${label}_zip_alignment_${dataOffset % ANDROID_16_KIB_PAGE_BYTES}`) } return true } function verifyApkNativeZipAlignment(archive, label) { const nativeNames = listNativeLibraryNames( archive, (name) => /^lib\/[^/]+\/[^/]+\.so$/.test(name), ) if (nativeNames.length === 0) fail(`${label}_zip_native_libraries_missing`) let storedLibrariesChecked = 0 let compressedLibrariesChecked = 0 for (const name of nativeNames) { const entryLabel = `${label}_${nativeEntryFailureLabel(name)}` const metadata = readLocalEntryMetadata(archive, name) if ((metadata.flags & 0x1) !== 0) fail(`${entryLabel}_zip_encrypted`) if (requireStoredNativeZipAlignment(metadata.method, metadata.dataOffset, entryLabel)) { storedLibrariesChecked += 1 } else { compressedLibrariesChecked += 1 } } return { zipAlignmentBytes: ANDROID_16_KIB_PAGE_BYTES, zipStoredLibrariesChecked: storedLibrariesChecked, zipCompressedLibrariesChecked: compressedLibrariesChecked, } } function parseBundlePageAlignment(configOutput) { let config try { config = JSON.parse(configOutput) } catch { fail('aab_bundle_config_json_unverifiable') } const nativeLibraryConfig = config?.optimizations?.uncompressNativeLibraries if ( !nativeLibraryConfig || typeof nativeLibraryConfig !== 'object' || Array.isArray(nativeLibraryConfig) ) { fail('aab_uncompress_native_libraries_config_missing') } if (nativeLibraryConfig.enabled !== true) { fail('aab_uncompress_native_libraries_not_enabled') } const alignment = nativeLibraryConfig.alignment if (typeof alignment !== 'string') fail('aab_page_alignment_unverifiable') if (alignment === 'PAGE_ALIGNMENT_UNSPECIFIED' || alignment === 'PAGE_ALIGNMENT_4K') { fail(`aab_page_alignment_insufficient_${alignment}`) } if (alignment !== 'PAGE_ALIGNMENT_16K') { fail(`aab_page_alignment_unverifiable_${alignment.replace(/[^a-z0-9]+/gi, '_')}`) } return { bundlePageAlignment: alignment, generatedApkZipAlignmentBytes: ANDROID_16_KIB_PAGE_BYTES, } } function locateAndroidTools() { const sdk = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME if (!sdk) fail('android_sdk_not_configured') const buildToolsRoot = join(sdk, 'build-tools') const versions = readdirSync(buildToolsRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .sort((left, right) => left.localeCompare(right, undefined, { numeric: true })) const selected = versions.at(-1) if (!selected) fail('android_build_tools_missing') const directory = join(buildToolsRoot, selected) const aapt = join(directory, process.platform === 'win32' ? 'aapt.exe' : 'aapt') const zipalign = join(directory, process.platform === 'win32' ? 'zipalign.exe' : 'zipalign') const apksignerJar = join(directory, 'lib', 'apksigner.jar') if (!existsSync(aapt) || !existsSync(apksignerJar)) fail('android_verification_tools_missing') return { aapt, apksignerJar, version: selected, zipalign } } function run(command, args, description) { const result = spawnSync(command, args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, windowsHide: true, env: { ...process.env, LANG: 'C', LC_ALL: 'C' }, }) if (result.status !== 0) { fail(`${description}_failed_${String(result.stderr || result.stdout).trim().replace(/\s+/g, '_')}`) } return result.stdout } function runArtifact(command, args, description, archive) { assertArchivePathStable(archive, description) const output = run(command, args, description) assertArchivePathStable(archive, description) return output } function runZipalign16KiB(archive, zipalign, description, runArtifactCommand = runArtifact) { if (!zipalign || !existsSync(zipalign)) fail(`${description}_tool_missing`) runArtifactCommand( zipalign, ['-c', '-P', '16', '-v', '4', archive.absolute], description, archive, ) } function verifyReleaseApkPageSize(archive, zipalign, runArtifactCommand = runArtifact) { const elfResult = verifyArchiveNativeElfAlignment( archive, (name) => /^lib\/[^/]+\/[^/]+\.so$/.test(name), 'release_apk', ) const zipEvidence = verifyApkNativeZipAlignment(archive, 'release_apk') runZipalign16KiB( archive, zipalign, 'release_zipalign_16kb', runArtifactCommand, ) return { ...elfResult.evidence, ...zipEvidence, zipalign16KiBVerified: true, } } function withVerifiedTemporaryDirectory(prefix, action) { const trustedTemporaryRoot = realpathSync(tmpdir()) const directory = mkdtempSync(join(trustedTemporaryRoot, prefix)) const resolvedDirectory = realpathSync(directory) if (!resolvedDirectory.startsWith(`${trustedTemporaryRoot}${sep}`)) { fail('temporary_directory_outside_system_temp') } try { return action(resolvedDirectory) } finally { const cleanupTarget = realpathSync(resolvedDirectory) if ( cleanupTarget !== resolvedDirectory || !cleanupTarget.startsWith(`${trustedTemporaryRoot}${sep}`) ) { fail('temporary_directory_cleanup_target_changed') } rmSync(cleanupTarget, { recursive: true, force: false }) } } function verifyAabGeneratedApkSetPageSize( archive, bundletoolPath, zipalign, sourceLibrarySha256s, ) { if (!zipalign || !existsSync(zipalign)) fail('aab_zipalign_tool_missing') return withVerifiedTemporaryDirectory('d3ro-aab-page-size-', (temporaryDirectory) => { const keyStorePath = join(temporaryDirectory, 'verification-signing.p12') const apkSetPath = join(temporaryDirectory, 'delivery.apks') const keyAlias = 'd3ro-16k-verifier' const password = randomBytes(24).toString('base64url') run('keytool', [ '-genkeypair', '-noprompt', '-keystore', keyStorePath, '-storetype', 'PKCS12', '-storepass', password, '-keypass', password, '-alias', keyAlias, '-keyalg', 'RSA', '-keysize', '2048', '-validity', '1', '-dname', 'CN=D3RO 16KiB Artifact Verification', ], 'aab_apk_set_ephemeral_key') runArtifact('java', [ '-jar', bundletoolPath, 'build-apks', `--bundle=${archive.absolute}`, `--output=${apkSetPath}`, `--ks=${keyStorePath}`, `--ks-pass=pass:${password}`, `--ks-key-alias=${keyAlias}`, `--key-pass=pass:${password}`, ], 'bundletool_build_apks', archive) const apkSetArchive = readZipDirectory(apkSetPath) try { const apkNames = [...apkSetArchive.entries.keys()] .filter((name) => /(^|\/)[^/]+\.apk$/.test(name)) .sort() if (apkNames.length === 0) fail('aab_apk_set_delivery_apks_missing') const deliveryLibrarySha256s = new Set() let nativeApkFilesChecked = 0 let elfLibrariesChecked = 0 let elfLoadSegmentsChecked = 0 let minimumElfLoadAlignment = null let zipStoredLibrariesChecked = 0 let zipCompressedLibrariesChecked = 0 for (let index = 0; index < apkNames.length; index += 1) { const apkBytes = extractEntry(apkSetArchive, apkNames[index]) const deliveryApkPath = join(temporaryDirectory, `delivery-${index}.apk`) writeFileSync(deliveryApkPath, apkBytes, { flag: 'wx', mode: 0o600 }) const deliveryArchive = readZipDirectory(deliveryApkPath) try { const nativeNames = listNativeLibraryNames( deliveryArchive, (name) => /^lib\/[^/]+\/[^/]+\.so$/.test(name), ) if (nativeNames.length === 0) continue const label = `aab_delivery_apk_${index}` const elfResult = verifyArchiveNativeElfAlignment( deliveryArchive, (name) => /^lib\/[^/]+\/[^/]+\.so$/.test(name), label, ) const zipEvidence = verifyApkNativeZipAlignment(deliveryArchive, label) runZipalign16KiB( deliveryArchive, zipalign, `${label}_zipalign_16kb`, ) for (const sha256 of elfResult.librarySha256s) deliveryLibrarySha256s.add(sha256) const alignment = BigInt(elfResult.evidence.minimumElfLoadAlignmentBytes) minimumElfLoadAlignment = minimumElfLoadAlignment === null || alignment < minimumElfLoadAlignment ? alignment : minimumElfLoadAlignment nativeApkFilesChecked += 1 elfLibrariesChecked += elfResult.evidence.elfLibrariesChecked elfLoadSegmentsChecked += elfResult.evidence.elfLoadSegmentsChecked zipStoredLibrariesChecked += zipEvidence.zipStoredLibrariesChecked zipCompressedLibrariesChecked += zipEvidence.zipCompressedLibrariesChecked } finally { if (deliveryArchive.keepOpen) closeSync(deliveryArchive.fd) } } if (nativeApkFilesChecked === 0) fail('aab_apk_set_native_delivery_apks_missing') for (const sha256 of new Set(sourceLibrarySha256s)) { if (!deliveryLibrarySha256s.has(sha256)) { fail(`aab_apk_set_source_native_missing_${sha256.slice(0, 16)}`) } } return { apkFilesInspected: apkNames.length, nativeApkFilesChecked, elfLibrariesChecked, elfLoadSegmentsChecked, minimumElfLoadAlignmentBytes: minimumElfLoadAlignment.toString(), zipAlignmentBytes: ANDROID_16_KIB_PAGE_BYTES, zipStoredLibrariesChecked, zipCompressedLibrariesChecked, sourceNativeLibrariesRepresented: new Set(sourceLibrarySha256s).size, zipalign16KiBVerified: true, } } finally { if (apkSetArchive.keepOpen) closeSync(apkSetArchive.fd) } }) } function extractElementBlocks(xmlTree, elementName) { const lines = xmlTree.split(/\r?\n/) const blocks = [] for (let index = 0; index < lines.length; index += 1) { const match = lines[index].match(/^(\s*)E:\s+([^\s(]+)/) if (!match || match[2] !== elementName) continue const indentation = match[1].length let end = index + 1 while (end < lines.length) { const next = lines[end].match(/^(\s*)E:\s+/) if (next && next[1].length <= indentation) break end += 1 } blocks.push(lines.slice(index, end).join('\n')) } return blocks } function rawAttribute(block, attribute) { const escaped = attribute.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') return block.match(new RegExp(`android:${escaped}[^\\n]*Raw:\\s*"([^"]+)"`))?.[1] ?? null } function filterHasExactData(block, expected) { const dataBlocks = extractElementBlocks(block, 'data') if (dataBlocks.length !== 1) return false const data = dataBlocks[0] return rawAttribute(data, 'scheme') === expected.scheme && rawAttribute(data, 'host') === expected.host && rawAttribute(data, 'pathPrefix') === (expected.pathPrefix ?? null) } function verifyDeepLinkManifest(manifest) { if (/android:scheme[^\n]*Raw:\s*"d3ro"/.test(manifest)) fail('legacy_d3ro_scheme_present') const activities = extractElementBlocks(manifest, 'activity') const mainActivity = activities.find((block) => ( rawAttribute(block, 'name') === 'com.d3ro.voice.MainActivity' )) if (!mainActivity || !/android:exported[^\n]*0xffffffff\b/.test(mainActivity)) { fail('main_activity_exported_contract') } const filters = extractElementBlocks(mainActivity, 'intent-filter') const hasBrowsableView = (block) => ( block.includes('android.intent.action.VIEW') && block.includes('android.intent.category.DEFAULT') && block.includes('android.intent.category.BROWSABLE') ) const authCallback = filters.some((block) => ( hasBrowsableView(block) && filterHasExactData(block, { scheme: 'd3ro-voice', host: 'auth-callback' }) )) if (!authCallback) fail('missing_deep_link_auth_callback') const inviteCallback = filters.some((block) => ( hasBrowsableView(block) && filterHasExactData(block, { scheme: 'd3ro-voice', host: 'accept-invite' }) )) if (!inviteCallback) fail('missing_deep_link_accept_invite') const verifiedAppLink = filters.some((block) => ( hasBrowsableView(block) && /android:autoVerify[^\n]*0xffffffff\b/.test(block) && filterHasExactData(block, { scheme: 'https', host: 'd3ro.chanpaca.net', pathPrefix: '/accept-invite', }) )) if (!verifiedAppLink) fail('missing_verified_app_link_accept_invite') } function verifyManifestContracts(badging, manifest, mode, expectedAdMobAppId) { const packageInfo = badging.match(/package: name='([^']+)' versionCode='([0-9]+)' versionName='([^']+)'/) if (!packageInfo || packageInfo[1] !== EXPECTED_PACKAGE) fail('package_name_mismatch') const versionCode = Number(packageInfo[2]) const versionName = packageInfo[3] const minSdk = Number(badging.match(/^sdkVersion:'([0-9]+)'$/m)?.[1]) const targetSdk = Number(badging.match(/^targetSdkVersion:'([0-9]+)'$/m)?.[1]) if (minSdk !== EXPECTED_MIN_SDK) fail('min_sdk_mismatch') if (targetSdk !== EXPECTED_TARGET_SDK) fail('target_sdk_mismatch') const debuggable = badging.includes('application-debuggable') if (mode !== 'debug' && debuggable) fail(`${mode}_must_not_be_debuggable`) if (mode === 'debug' && !debuggable) fail('debug_must_be_debuggable') if (!/android:usesCleartextTraffic[^\n]*0x0\b/.test(manifest) && mode !== 'debug') { fail(`${mode}_cleartext_must_be_false`) } const requiredPermissions = [ 'android.permission.INTERNET', 'android.permission.RECORD_AUDIO', 'android.permission.POST_NOTIFICATIONS', 'android.permission.FOREGROUND_SERVICE_MICROPHONE', ] for (const permission of requiredPermissions) { if (!manifest.includes(permission)) fail(`missing_permission_${permission}`) } verifyDeepLinkManifest(manifest) const adMetadata = manifest.match( /com\.google\.android\.gms\.ads\.APPLICATION_ID[\s\S]{0,600}?android:value[^\n]*Raw:\s*"([^"]+)"/, ) if (!adMetadata) fail('admob_application_id_missing') const adMobAppId = adMetadata[1] if (mode === 'release') { if (!/^ca-app-pub-\d+~\d+$/.test(adMobAppId)) fail('release_admob_application_id_format') if (adMobAppId === TEST_ADMOB_APP_ID) fail('release_uses_test_admob_application_id') } else if (adMobAppId !== TEST_ADMOB_APP_ID) { fail(`${mode}_must_use_google_test_admob_application_id`) } if (expectedAdMobAppId && adMobAppId !== expectedAdMobAppId) fail('admob_application_id_env_mismatch') return { debuggable, adMobAppId, versionName, versionCode, minSdk, targetSdk } } function verifyManifest(aapt, archive, mode, expectedAdMobAppId) { const apkPath = archive.absolute const badging = runArtifact(aapt, ['dump', 'badging', apkPath], 'aapt_badging', archive) const manifest = runArtifact(aapt, ['dump', 'xmltree', apkPath, 'AndroidManifest.xml'], 'aapt_manifest', archive) return verifyManifestContracts(badging, manifest, mode, expectedAdMobAppId) } function verifyArchive(archive, mode) { const names = [...archive.entries.keys()] const bundle = mode === 'debug' ? null : extractEntry(archive, 'assets/index.android.bundle') if (bundle !== null && bundle.length < 100_000) fail('embedded_bundle_too_small') const model = extractEntry(archive, MODEL_PATH) if (model.length !== MODEL_BYTES) fail('whisper_model_size') const modelSha256 = createHash('sha256').update(model).digest('hex') if (modelSha256 !== MODEL_SHA256) fail('whisper_model_sha256') const abiSet = new Set( names .map((name) => name.match(/^lib\/([^/]+)\//)?.[1]) .filter(Boolean), ) const requiredAbis = mode === 'release' ? ['arm64-v8a'] : mode === 'e2e' ? ['arm64-v8a', 'x86_64'] : ['x86_64'] for (const requiredAbi of requiredAbis) { if (!abiSet.has(requiredAbi)) fail(`missing_abi_${requiredAbi}`) } for (const abi of abiSet) { for (const library of REQUIRED_NATIVE_LIBRARIES) { if (!archive.entries.has(`lib/${abi}/${library}`)) { fail(`missing_native_${abi}_${library}`) } } if (abi === 'x86_64' && !archive.entries.has(`lib/${abi}/librnwhisper_x86_64.so`)) { fail('missing_native_x86_64_librnwhisper_x86_64.so') } if (abi === 'arm64-v8a') { for (const optimizedWhisper of ['librnwhisper_v8.so', 'librnwhisper_v8fp16_va_2.so']) { if (!archive.entries.has(`lib/${abi}/${optimizedWhisper}`)) { fail(`missing_native_${abi}_${optimizedWhisper}`) } } } if (mode !== 'debug') { const reactNative = extractEntry(archive, `lib/${abi}/libreactnative.so`) if (reactNative.includes(DEBUG_PROPS_SYMBOL)) { fail(`${mode}_native_${abi}_uses_debug_props_abi`) } } } if (mode === 'e2e' && ( abiSet.size !== 2 || !abiSet.has('arm64-v8a') || !abiSet.has('x86_64') )) { fail(`e2e_abi_set_${[...abiSet].sort().join('_')}`) } if (mode === 'release' && (abiSet.size !== 1 || !abiSet.has('arm64-v8a'))) { fail(`release_abi_set_${[...abiSet].sort().join('_')}`) } return { bundleBytes: bundle?.length ?? 0, modelBytes: model.length, modelSha256, abis: [...abiSet].sort(), } } function parseAabManifest(manifest) { const packageName = manifest.match(/\bpackage="([^"]+)"/)?.[1] const versionName = manifest.match(/\bandroid:versionName="([^"]+)"/)?.[1] const versionCodeText = manifest.match(/\bandroid:versionCode="([0-9]+)"/)?.[1] const metadataTag = [...manifest.matchAll(/]*>/g)] .map((match) => match[0]) .find((tag) => /android:name="com\.google\.android\.gms\.ads\.APPLICATION_ID"/.test(tag)) const adMobAppId = metadataTag?.match(/\bandroid:value="([^"]+)"/)?.[1] if (!packageName || !versionName || !versionCodeText || !adMobAppId) { fail('aab_manifest_metadata_missing') } return { packageName, versionName, versionCode: Number(versionCodeText), adMobAppId, debuggable: /]*\bandroid:debuggable="true"/.test(manifest), } } function verifyReleaseBundle(path, bundletoolPath, zipalign, expected) { const archive = readZipDirectory(path) try { const names = [...archive.entries.keys()] const bundle = extractEntry(archive, 'base/assets/index.android.bundle') if (bundle.length < 100_000) fail('aab_embedded_bundle_too_small') const model = extractEntry(archive, `base/${MODEL_PATH}`) if (model.length !== MODEL_BYTES) fail('aab_whisper_model_size') const modelSha256 = createHash('sha256').update(model).digest('hex') if (modelSha256 !== MODEL_SHA256) fail('aab_whisper_model_sha256') const abiSet = new Set( names .map((name) => name.match(/^[^/]+\/lib\/([^/]+)\//)?.[1]) .filter(Boolean), ) if (abiSet.size !== 1 || !abiSet.has('arm64-v8a')) { fail(`aab_abi_set_${[...abiSet].sort().join('_')}`) } const elfPageSizeResult = verifyArchiveNativeElfAlignment( archive, (name) => /^[^/]+\/lib\/[^/]+\/[^/]+\.so$/.test(name), 'aab', ) for (const library of [ ...REQUIRED_NATIVE_LIBRARIES, 'librnwhisper_v8.so', 'librnwhisper_v8fp16_va_2.so', ]) { if (!archive.entries.has(`base/lib/arm64-v8a/${library}`)) { fail(`aab_missing_native_arm64-v8a_${library}`) } } const reactNative = extractEntry(archive, 'base/lib/arm64-v8a/libreactnative.so') if (reactNative.includes(DEBUG_PROPS_SYMBOL)) { fail('aab_native_arm64-v8a_uses_debug_props_abi') } const bundleConfig = runArtifact( 'java', ['-jar', bundletoolPath, 'dump', 'config', `--bundle=${archive.absolute}`], 'bundletool_config', archive, ) const bundlePageSizeEvidence = parseBundlePageAlignment(bundleConfig) const deliveryApkSetPageSizeEvidence = verifyAabGeneratedApkSetPageSize( archive, bundletoolPath, zipalign, elfPageSizeResult.librarySha256s, ) const manifest = runArtifact( 'java', ['-jar', bundletoolPath, 'dump', 'manifest', `--bundle=${archive.absolute}`, '--module=base'], 'bundletool_manifest', archive, ) const manifestEvidence = parseAabManifest(manifest) if (manifestEvidence.packageName !== EXPECTED_PACKAGE) fail('aab_package_name_mismatch') if (manifestEvidence.versionName !== expected.versionName) fail('aab_version_name_mismatch') if (manifestEvidence.versionCode !== expected.versionCode) fail('aab_version_code_mismatch') if (manifestEvidence.debuggable) fail('aab_must_not_be_debuggable') if (manifestEvidence.adMobAppId !== expected.adMobAppId) fail('aab_admob_application_id_mismatch') if (manifestEvidence.adMobAppId === TEST_ADMOB_APP_ID) fail('aab_uses_test_admob_application_id') runArtifact('jarsigner', ['-verify', '-strict', archive.absolute], 'jarsigner', archive) const certificate = runArtifact( 'keytool', ['-printcert', '-jarfile', archive.absolute], 'aab_certificate', archive, ) const signerText = certificate.match(/SHA-?256:\s*([0-9A-F:]+)/i)?.[1] if (!signerText) fail('aab_signer_sha256_missing') const signerSha256 = requireAllowedReleaseCertificate(signerText, 'aab_signer_sha256') if (signerSha256 !== expected.signerSha256) fail('aab_signer_sha256_mismatch') return { artifact: basename(archive.absolute), sha256: hashArchiveStable(archive, 'aab'), ...manifestEvidence, signerSha256, bundleBytes: bundle.length, modelBytes: model.length, modelSha256, abis: [...abiSet], pageSize: { ...elfPageSizeResult.evidence, ...bundlePageSizeEvidence, deliveryApkSet: deliveryApkSetPageSizeEvidence, }, } } finally { if (archive.keepOpen) closeSync(archive.fd) } } function createElf64SelfTestFixture(alignments, types = alignments.map(() => ELF_PT_LOAD)) { const programHeaderBytes = 56 const buffer = Buffer.alloc(64 + alignments.length * programHeaderBytes) buffer.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1], 0) buffer.writeUInt16LE(3, 16) buffer.writeUInt16LE(183, 18) buffer.writeUInt32LE(1, 20) buffer.writeBigUInt64LE(64n, 32) buffer.writeUInt16LE(64, 52) buffer.writeUInt16LE(programHeaderBytes, 54) buffer.writeUInt16LE(alignments.length, 56) for (let index = 0; index < alignments.length; index += 1) { const offset = 64 + index * programHeaderBytes buffer.writeUInt32LE(types[index], offset) buffer.writeBigUInt64LE(BigInt(alignments[index]), offset + 48) } return buffer } function createStoredZipSelfTestFixture({ data, alignment, centralName = 'lib/arm64-v8a/libfixture.so', localName = centralName, flags = 0, centralMethod = 0, localMethod = centralMethod, }) { const localNameBytes = Buffer.from(localName) const centralNameBytes = Buffer.from(centralName) const extraLength = ( alignment - ((30 + localNameBytes.length) % alignment) ) % alignment const localHeader = Buffer.alloc(30 + localNameBytes.length + extraLength) localHeader.writeUInt32LE(0x04034b50, 0) localHeader.writeUInt16LE(20, 4) localHeader.writeUInt16LE(flags, 6) localHeader.writeUInt16LE(localMethod, 8) localHeader.writeUInt32LE(data.length, 18) localHeader.writeUInt32LE(data.length, 22) localHeader.writeUInt16LE(localNameBytes.length, 26) localHeader.writeUInt16LE(extraLength, 28) localNameBytes.copy(localHeader, 30) const centralOffset = localHeader.length + data.length const centralHeader = Buffer.alloc(46 + centralNameBytes.length) centralHeader.writeUInt32LE(0x02014b50, 0) centralHeader.writeUInt16LE(20, 4) centralHeader.writeUInt16LE(20, 6) centralHeader.writeUInt16LE(flags, 8) centralHeader.writeUInt16LE(centralMethod, 10) centralHeader.writeUInt32LE(data.length, 20) centralHeader.writeUInt32LE(data.length, 24) centralHeader.writeUInt16LE(centralNameBytes.length, 28) centralHeader.writeUInt32LE(0, 42) centralNameBytes.copy(centralHeader, 46) const endOfDirectory = Buffer.alloc(22) endOfDirectory.writeUInt32LE(0x06054b50, 0) endOfDirectory.writeUInt16LE(1, 8) endOfDirectory.writeUInt16LE(1, 10) endOfDirectory.writeUInt32LE(centralHeader.length, 12) endOfDirectory.writeUInt32LE(centralOffset, 16) return Buffer.concat([localHeader, data, centralHeader, endOfDirectory]) } function withZipSelfTestArchive(buffer, action) { return withVerifiedTemporaryDirectory('d3ro-zip-self-test-', (temporaryDirectory) => { const path = join(temporaryDirectory, 'fixture.apk') writeFileSync(path, buffer, { flag: 'wx', mode: 0o600 }) const archive = readZipDirectory(path) try { return action(archive) } finally { if (archive.keepOpen) closeSync(archive.fd) } }) } function expectSelfTestFailure(label, action, expectedMessage) { let rejected = false try { action() } catch (error) { rejected = error instanceof Error && error.message.includes(expectedMessage) } if (!rejected) fail(`self_test_${label}_not_rejected`) } function createManifestContractSelfTestFixture() { const badging = [ "package: name='com.d3ro.voice' versionCode='1000001' versionName='1.0.1'", "sdkVersion:'24'", "targetSdkVersion:'36'", ].join('\n') const manifest = [ 'E: manifest', ' E: uses-permission', ' A: android:name="android.permission.INTERNET" (Raw: "android.permission.INTERNET")', ' E: uses-permission', ' A: android:name="android.permission.RECORD_AUDIO" (Raw: "android.permission.RECORD_AUDIO")', ' E: uses-permission', ' A: android:name="android.permission.POST_NOTIFICATIONS" (Raw: "android.permission.POST_NOTIFICATIONS")', ' E: uses-permission', ' A: android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" (Raw: "android.permission.FOREGROUND_SERVICE_MICROPHONE")', ' E: application', ' A: android:usesCleartextTraffic=(type 0x12)0x0', ' E: meta-data', ' A: android:name="com.google.android.gms.ads.APPLICATION_ID" (Raw: "com.google.android.gms.ads.APPLICATION_ID")', ' A: android:value="ca-app-pub-1234567890123456~1234567890" (Raw: "ca-app-pub-1234567890123456~1234567890")', ' E: activity', ' A: android:name="com.d3ro.voice.MainActivity" (Raw: "com.d3ro.voice.MainActivity")', ' A: android:exported=(type 0x12)0xffffffff', ' E: intent-filter', ' E: action', ' A: android:name="android.intent.action.VIEW" (Raw: "android.intent.action.VIEW")', ' E: category', ' A: android:name="android.intent.category.DEFAULT" (Raw: "android.intent.category.DEFAULT")', ' E: category', ' A: android:name="android.intent.category.BROWSABLE" (Raw: "android.intent.category.BROWSABLE")', ' E: data', ' A: android:scheme="d3ro-voice" (Raw: "d3ro-voice")', ' A: android:host="auth-callback" (Raw: "auth-callback")', ' E: intent-filter', ' E: action', ' A: android:name="android.intent.action.VIEW" (Raw: "android.intent.action.VIEW")', ' E: category', ' A: android:name="android.intent.category.DEFAULT" (Raw: "android.intent.category.DEFAULT")', ' E: category', ' A: android:name="android.intent.category.BROWSABLE" (Raw: "android.intent.category.BROWSABLE")', ' E: data', ' A: android:scheme="d3ro-voice" (Raw: "d3ro-voice")', ' A: android:host="accept-invite" (Raw: "accept-invite")', ' E: intent-filter', ' A: android:autoVerify=(type 0x12)0xffffffff', ' E: action', ' A: android:name="android.intent.action.VIEW" (Raw: "android.intent.action.VIEW")', ' E: category', ' A: android:name="android.intent.category.DEFAULT" (Raw: "android.intent.category.DEFAULT")', ' E: category', ' A: android:name="android.intent.category.BROWSABLE" (Raw: "android.intent.category.BROWSABLE")', ' E: data', ' A: android:scheme="https" (Raw: "https")', ' A: android:host="d3ro.chanpaca.net" (Raw: "d3ro.chanpaca.net")', ' A: android:pathPrefix="/accept-invite" (Raw: "/accept-invite")', ].join('\n') return { badging, manifest } } function runSigningPolicySelfTest() { const safe = '4F:AC:69:24:82:1C:50:DA:AB:ED:76:49:32:A5:3C:48:6F:8C:6C:5F:34:B9:F1:8D:B9:20:AA:40:99:15:2B:54' if (requireAllowedReleaseCertificate(safe, 'self_test_safe') !== safe.replaceAll(':', '').toLowerCase()) { fail('self_test_safe_certificate_mismatch') } let compromisedRejected = false try { requireAllowedReleaseCertificate(COMPROMISED_SIGNER_SHA256, 'self_test') } catch (error) { compromisedRejected = error instanceof Error && error.message.includes('self_test_compromised') } if (!compromisedRejected) fail('self_test_compromised_certificate_not_rejected') let legacyArgumentRejected = false try { parseArguments(['--expected-cert-sha256', safe]) } catch (error) { legacyArgumentRejected = error instanceof Error && error.message.includes('unexpected_argument_expected-cert-sha256') } if (!legacyArgumentRejected) fail('self_test_legacy_certificate_argument_not_rejected') const manifestFixture = createManifestContractSelfTestFixture() const manifestEvidence = verifyManifestContracts( manifestFixture.badging, manifestFixture.manifest, 'release', 'ca-app-pub-1234567890123456~1234567890', ) if (manifestEvidence.minSdk !== EXPECTED_MIN_SDK || manifestEvidence.targetSdk !== EXPECTED_TARGET_SDK) { fail('self_test_manifest_sdk_evidence') } expectSelfTestFailure( 'manifest_min_sdk', () => verifyManifestContracts( manifestFixture.badging.replace("sdkVersion:'24'", "sdkVersion:'23'"), manifestFixture.manifest, 'release', 'ca-app-pub-1234567890123456~1234567890', ), 'min_sdk_mismatch', ) expectSelfTestFailure( 'manifest_decoy_auth_host', () => verifyManifestContracts( manifestFixture.badging, `${manifestFixture.manifest.replace( 'android:host="auth-callback" (Raw: "auth-callback")', 'android:host="decoy" (Raw: "decoy")', )}\n E: meta-data\n A: android:value="auth-callback" (Raw: "auth-callback")`, 'release', 'ca-app-pub-1234567890123456~1234567890', ), 'missing_deep_link_auth_callback', ) expectSelfTestFailure( 'manifest_legacy_scheme', () => verifyManifestContracts( manifestFixture.badging, manifestFixture.manifest.replace( 'android:scheme="d3ro-voice" (Raw: "d3ro-voice")', 'android:scheme="d3ro" (Raw: "d3ro")', ), 'release', 'ca-app-pub-1234567890123456~1234567890', ), 'legacy_d3ro_scheme_present', ) expectSelfTestFailure( 'manifest_app_link_not_verified', () => verifyManifestContracts( manifestFixture.badging, manifestFixture.manifest.replace( 'android:autoVerify=(type 0x12)0xffffffff', 'android:autoVerify=(type 0x12)0x0', ), 'release', 'ca-app-pub-1234567890123456~1234567890', ), 'missing_verified_app_link_accept_invite', ) const alignedElf = verifyElfLoadSegmentAlignment( createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES, 64 * 1024]), 'self_test_aligned', ) if ( alignedElf.loadSegmentCount !== 2 || alignedElf.minimumLoadAlignmentBytes !== String(ANDROID_16_KIB_PAGE_BYTES) ) { fail('self_test_aligned_elf_evidence') } const elf32 = createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES]) elf32[4] = 1 expectSelfTestFailure( 'elf_32_unverifiable', () => verifyElfLoadSegmentAlignment(elf32, 'self_test_elf32'), 'self_test_elf32_elf_class_1', ) const bigEndianElf = createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES]) bigEndianElf[5] = 2 expectSelfTestFailure( 'elf_big_endian_unverifiable', () => verifyElfLoadSegmentAlignment(bigEndianElf, 'self_test_big_endian'), 'self_test_big_endian_elf_data_encoding_2', ) const wrongMachineElf = createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES]) wrongMachineElf.writeUInt16LE(62, 18) expectSelfTestFailure( 'elf_wrong_machine', () => verifyElfLoadSegmentAlignment(wrongMachineElf, 'self_test_machine'), 'self_test_machine_elf_machine_62', ) const wrongTypeElf = createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES]) wrongTypeElf.writeUInt16LE(0, 16) expectSelfTestFailure( 'elf_wrong_type', () => verifyElfLoadSegmentAlignment(wrongTypeElf, 'self_test_type'), 'self_test_type_elf_type_0', ) expectSelfTestFailure( 'elf_4k_only', () => verifyElfLoadSegmentAlignment(createElf64SelfTestFixture([4 * 1024]), 'self_test_4k'), 'self_test_4k_elf_load_0_alignment_4096', ) expectSelfTestFailure( 'elf_non_load', () => verifyElfLoadSegmentAlignment( createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES], [0]), 'self_test_non_load', ), 'self_test_non_load_elf_load_segments_missing', ) const misalignedElf = createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES]) misalignedElf.writeBigUInt64LE(1n, 64 + 16) expectSelfTestFailure( 'elf_offset_address', () => verifyElfLoadSegmentAlignment(misalignedElf, 'self_test_offset_address'), 'self_test_offset_address_elf_load_0_offset_address_misaligned', ) expectSelfTestFailure( 'elf_truncated', () => verifyElfLoadSegmentAlignment( createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES]).subarray(0, 80), 'self_test_truncated', ), 'self_test_truncated_elf_program_header_table_truncated', ) const bundle16KiB = parseBundlePageAlignment(JSON.stringify({ optimizations: { uncompressNativeLibraries: { enabled: true, alignment: 'PAGE_ALIGNMENT_16K' }, }, })) if (bundle16KiB.generatedApkZipAlignmentBytes !== ANDROID_16_KIB_PAGE_BYTES) { fail('self_test_bundle_16k_evidence') } expectSelfTestFailure( 'bundle_64k_not_exact_contract', () => parseBundlePageAlignment(JSON.stringify({ optimizations: { uncompressNativeLibraries: { enabled: true, alignment: 'PAGE_ALIGNMENT_64K' }, }, })), 'aab_page_alignment_unverifiable_PAGE_ALIGNMENT_64K', ) expectSelfTestFailure( 'bundle_4k_only', () => parseBundlePageAlignment(JSON.stringify({ optimizations: { uncompressNativeLibraries: { enabled: true, alignment: 'PAGE_ALIGNMENT_4K' }, }, })), 'aab_page_alignment_insufficient_PAGE_ALIGNMENT_4K', ) expectSelfTestFailure( 'bundle_unspecified', () => parseBundlePageAlignment(JSON.stringify({ optimizations: { uncompressNativeLibraries: { enabled: true, alignment: 'PAGE_ALIGNMENT_UNSPECIFIED' }, }, })), 'aab_page_alignment_insufficient_PAGE_ALIGNMENT_UNSPECIFIED', ) expectSelfTestFailure( 'bundle_alignment_missing', () => parseBundlePageAlignment(JSON.stringify({ compression: { uncompressedGlob: ['PAGE_ALIGNMENT_16K'] }, optimizations: { uncompressNativeLibraries: { enabled: true } }, })), 'aab_page_alignment_unverifiable', ) expectSelfTestFailure( 'bundle_alignment_wrong_field', () => parseBundlePageAlignment(JSON.stringify({ alignment: 'PAGE_ALIGNMENT_16K', optimizations: { uncompressNativeLibraries: { enabled: true } }, })), 'aab_page_alignment_unverifiable', ) expectSelfTestFailure( 'bundle_native_libraries_disabled', () => parseBundlePageAlignment(JSON.stringify({ optimizations: { uncompressNativeLibraries: { enabled: false, alignment: 'PAGE_ALIGNMENT_16K' }, }, })), 'aab_uncompress_native_libraries_not_enabled', ) expectSelfTestFailure( 'bundle_config_non_json', () => parseBundlePageAlignment('alignment: PAGE_ALIGNMENT_16K'), 'aab_bundle_config_json_unverifiable', ) if (!requireStoredNativeZipAlignment(0, ANDROID_16_KIB_PAGE_BYTES, 'self_test_zip_aligned')) { fail('self_test_zip_aligned_evidence') } if (requireStoredNativeZipAlignment(8, 1, 'self_test_zip_compressed')) { fail('self_test_zip_compressed_evidence') } expectSelfTestFailure( 'zip_4k_only', () => requireStoredNativeZipAlignment(0, 4 * 1024, 'self_test_zip_4k'), 'self_test_zip_4k_zip_alignment_4096', ) expectSelfTestFailure( 'zip_method_unverifiable', () => requireStoredNativeZipAlignment(12, ANDROID_16_KIB_PAGE_BYTES, 'self_test_zip_method'), 'self_test_zip_method_zip_method_12_unverifiable', ) const elfFixture = createElf64SelfTestFixture([ANDROID_16_KIB_PAGE_BYTES]) let zipalignInvocation = null const archivePageSizeEvidence = withZipSelfTestArchive( createStoredZipSelfTestFixture({ data: elfFixture, alignment: ANDROID_16_KIB_PAGE_BYTES, }), (archive) => verifyReleaseApkPageSize( archive, process.execPath, (command, args, description, invokedArchive) => { zipalignInvocation = { command, args, description, invokedArchive } return '' }, ), ) if ( archivePageSizeEvidence.elfLibrariesChecked !== 1 || archivePageSizeEvidence.zipStoredLibrariesChecked !== 1 || archivePageSizeEvidence.zipCompressedLibrariesChecked !== 0 ) { fail('self_test_zip_archive_evidence') } if ( zipalignInvocation?.command !== process.execPath || zipalignInvocation.description !== 'release_zipalign_16kb' || zipalignInvocation.args.length !== 6 || zipalignInvocation.args[0] !== '-c' || zipalignInvocation.args[1] !== '-P' || zipalignInvocation.args[2] !== '16' || zipalignInvocation.args[3] !== '-v' || zipalignInvocation.args[4] !== '4' || zipalignInvocation.args[5] !== zipalignInvocation.invokedArchive.absolute ) { fail('self_test_zipalign_invocation') } let fourKiBZipalignInvoked = false expectSelfTestFailure( 'zip_archive_4k_only', () => withZipSelfTestArchive( createStoredZipSelfTestFixture({ data: elfFixture, alignment: 4 * 1024 }), (archive) => verifyReleaseApkPageSize( archive, process.execPath, () => { fourKiBZipalignInvoked = true return '' }, ), ), 'zip_alignment_4096', ) if (fourKiBZipalignInvoked) fail('self_test_zip_archive_4k_reached_zipalign') expectSelfTestFailure( 'zip_archive_local_name_mismatch', () => withZipSelfTestArchive( createStoredZipSelfTestFixture({ data: elfFixture, alignment: ANDROID_16_KIB_PAGE_BYTES, localName: 'lib/arm64-v8a/libfixturE.so', }), (archive) => verifyReleaseApkPageSize(archive, process.execPath, () => ''), ), 'local_name_mismatch_lib/arm64-v8a/libfixture.so', ) expectSelfTestFailure( 'zip_archive_encrypted', () => withZipSelfTestArchive( createStoredZipSelfTestFixture({ data: elfFixture, alignment: ANDROID_16_KIB_PAGE_BYTES, flags: 1, }), (archive) => verifyReleaseApkPageSize(archive, process.execPath, () => ''), ), 'zip_encrypted', ) expectSelfTestFailure( 'zip_archive_method_mismatch', () => withZipSelfTestArchive( createStoredZipSelfTestFixture({ data: elfFixture, alignment: ANDROID_16_KIB_PAGE_BYTES, centralMethod: 0, localMethod: 8, }), (archive) => verifyReleaseApkPageSize(archive, process.execPath, () => ''), ), 'local_method_mismatch_lib/arm64-v8a/libfixture.so', ) process.stdout.write(`${JSON.stringify({ ok: true, compromisedCertificateRejected: true, manifestContractVerified: true, elf16KiBVerified: true, bundleAlignment16KiBVerified: true, zipAlignment16KiBVerified: true, })}\n`) } const cliArguments = process.argv.slice(2) if (cliArguments.length === 1 && cliArguments[0] === '--self-test') { runSigningPolicySelfTest() process.exit(0) } const options = parseArguments(cliArguments) const mode = options.mode if (!VALID_MODES.has(mode)) fail(`unknown_mode_${mode}`) if (!options.apk) fail('apk_path_required') const apkPath = resolve(options.apk) if (!existsSync(apkPath)) fail(`apk_missing_${apkPath}`) if (options.aab && mode !== 'release') fail('aab_only_valid_for_release') const aabPath = options.aab ? resolve(options.aab) : null if (aabPath && !existsSync(aabPath)) fail(`aab_missing_${aabPath}`) const tools = locateAndroidTools() const bundletoolPath = options.bundletool ? resolve(options.bundletool) : null if (mode === 'release' && aabPath && !bundletoolPath) fail('bundletool_path_required') if (bundletoolPath) { const bundletoolStat = lstatSync(bundletoolPath, { bigint: true }) if (!bundletoolStat.isFile() || bundletoolStat.isSymbolicLink() || bundletoolStat.nlink !== 1n) { fail('bundletool_path_invalid') } if (realpathSync(bundletoolPath) !== bundletoolPath) fail('bundletool_reparse_rejected') } const archive = readZipDirectory(apkPath) try { const archiveEvidence = verifyArchive(archive, mode) const pageSizeEvidence = mode === 'release' ? verifyReleaseApkPageSize(archive, tools.zipalign) : null const manifestEvidence = verifyManifest( tools.aapt, archive, mode, options['expected-admob-app-id'], ) if (mode !== 'debug') { const expectedVersionName = options['expected-version-name'] const expectedVersionCode = Number(options['expected-version-code']) if (!expectedVersionName) fail(`${mode}_expected_version_name_missing`) if (!Number.isSafeInteger(expectedVersionCode) || expectedVersionCode <= 0) { fail(`${mode}_expected_version_code_invalid`) } if (manifestEvidence.versionName !== expectedVersionName) fail(`${mode}_version_name_mismatch`) if (manifestEvidence.versionCode !== expectedVersionCode) fail(`${mode}_version_code_mismatch`) } const signature = runArtifact( 'java', ['-jar', tools.apksignerJar, 'verify', '--verbose', '--print-certs', apkPath], 'apksigner', archive, ) if (mode === 'release' && /CN=Android Debug/i.test(signature)) fail('release_debug_certificate') const expectedCertificate = mode === 'release' ? requireAllowedReleaseCertificate(options['expected-upload-cert-sha256'], 'release_expected_upload_certificate') : options['expected-upload-cert-sha256'] ? normalizeCertificateSha256(options['expected-upload-cert-sha256'], 'expected_upload_certificate') : null const actualCertificateText = signature .match(/Signer #1 certificate SHA-256 digest:\s*([0-9a-f:]+)/i)?.[1] if (!actualCertificateText) fail('signer_sha256_missing') const actualCertificate = mode === 'release' ? requireAllowedReleaseCertificate(actualCertificateText, 'release_signer_sha256') : normalizeCertificateSha256(actualCertificateText, 'signer_sha256') if (expectedCertificate && actualCertificate !== expectedCertificate) fail('signer_sha256_mismatch') const apkSha256 = hashArchiveStable(archive, 'apk') const aabEvidence = aabPath ? verifyReleaseBundle(aabPath, bundletoolPath, tools.zipalign, { versionName: manifestEvidence.versionName, versionCode: manifestEvidence.versionCode, adMobAppId: manifestEvidence.adMobAppId, signerSha256: actualCertificate, }) : null process.stdout.write(`${JSON.stringify({ artifact: basename(apkPath), mode, packageName: EXPECTED_PACKAGE, buildTools: tools.version, apkSha256, signerSha256: actualCertificate, ...manifestEvidence, ...archiveEvidence, ...(pageSizeEvidence ? { pageSize: pageSizeEvidence } : {}), aab: aabEvidence, }, null, 2)}\n`) } finally { if (archive.keepOpen) closeSync(archive.fd) }