iOSBuildServer/backend/automation/obfuscation/generate_image_whitelist.py

203 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
构建期 dylib 白名单生成脚本
优先扫描最终 .app Bundle 内实际嵌入并会被 dyld 加载的镜像,生成:
1. RDKnownImages.plist — 白名单 + HMAC-SHA256 摘要
2. Generated/RDWhitelistSecret.swift — 仅在缺失时补生成
在 Xcode Build Phase 中调用时,会复用现有密钥并更新最终 app bundle 内的 plist
手动执行时若无法定位 app bundle则退回扫描仓库中的 framework/dylib 来源。
"""
import os
import sys
import hmac
import hashlib
import plistlib
import secrets
import re
from pathlib import Path
# 所有 framework 来源目录(相对于项目根目录)
FRAMEWORK_SEARCH_PATHS = [
'Pods',
'readoor/BookShelf/Classes/Factorys',
'readoor/3.0/Factorys',
'readoor/3.0/Features/ThirdPartyService',
]
# 输出路径
PLIST_OUTPUT = 'readoor/3.0/Base/Security/RDKnownImages.plist'
SECRET_OUTPUT = 'readoor/3.0/Base/Security/Generated/RDWhitelistSecret.swift'
def scan_source_frameworks(search_paths, app_name):
"""扫描仓库中的 framework 和 dylib 来源,作为无法定位 app bundle 时的兜底方案。"""
known = []
seen = set()
for search_dir in search_paths:
if not os.path.isdir(search_dir):
continue
for root, dirs, files in os.walk(search_dir):
# .framework 在 macOS 上是目录,出现在 dirs 中
for d in dirs:
if d.endswith('.framework'):
basename = d.replace('.framework', '')
rel_path = os.path.relpath(os.path.join(root, d), '.')
key = (basename, rel_path)
if key not in seen:
seen.add(key)
known.append({'name': basename, 'path': rel_path})
for f in files:
if f.endswith('.dylib'):
basename = f.replace('.dylib', '')
rel_path = os.path.relpath(os.path.join(root, f), '.')
key = (basename, rel_path)
if key not in seen:
seen.add(key)
known.append({'name': basename, 'path': rel_path})
# App 自身
app_key = (app_name, app_name)
if app_key not in seen:
known.append({'name': app_name, 'path': app_name})
# 按 name 排序保证稳定性
known.sort(key=lambda x: x['name'])
return known
def scan_bundled_images(app_bundle_path, executable_path):
"""扫描最终 app bundle 中会实际被加载的镜像,路径使用 bundle 相对路径。"""
known = []
seen = set()
executable_name = Path(executable_path).name if executable_path else Path(app_bundle_path).stem
main_key = (executable_name, executable_path or executable_name)
seen.add(main_key)
known.append({'name': executable_name, 'path': executable_path or executable_name})
for root, dirs, files in os.walk(app_bundle_path):
dirs.sort()
files.sort()
for d in dirs:
if not d.endswith('.framework'):
continue
framework_dir = os.path.join(root, d)
basename = d.replace('.framework', '')
# if basename == 'MJExtension': # 故意排除
# continue
binary_path = os.path.join(framework_dir, basename)
if not os.path.isfile(binary_path):
continue
rel_path = os.path.relpath(binary_path, app_bundle_path)
key = (basename, rel_path)
if key not in seen:
seen.add(key)
known.append({'name': basename, 'path': rel_path})
for f in files:
if not f.endswith('.dylib'):
continue
dylib_path = os.path.join(root, f)
basename = f.replace('.dylib', '')
rel_path = os.path.relpath(dylib_path, app_bundle_path)
key = (basename, rel_path)
if key not in seen:
seen.add(key)
known.append({'name': basename, 'path': rel_path})
known.sort(key=lambda x: (x['name'], x['path']))
return known
def compute_digest(entries, secret):
"""对排序后的白名单内容计算 HMAC-SHA256 摘要"""
parts = [f"{e['name']}|{e['path']}" for e in entries]
message = '\n'.join(parts).encode('utf-8')
return hmac.new(secret, message, hashlib.sha256).hexdigest()
def generate_swift_secret(secret_bytes):
"""将构建期密钥写入 Swift 源码"""
hex_bytes = ', '.join(f'0x{b:02x}' for b in secret_bytes)
source = f"""// Auto-generated by generate_image_whitelist.py — do not edit manually
import Foundation
enum RDWhitelistSecret {{
static let key: [UInt8] = [{hex_bytes}]
}}
"""
os.makedirs(os.path.dirname(SECRET_OUTPUT), exist_ok=True)
with open(SECRET_OUTPUT, 'w') as f:
f.write(source)
def load_existing_secret():
if not os.path.isfile(SECRET_OUTPUT):
return None
with open(SECRET_OUTPUT, 'r', encoding='utf-8') as f:
content = f.read()
hex_values = re.findall(r'0x([0-9a-fA-F]{2})', content)
if not hex_values:
return None
return bytes(int(value, 16) for value in hex_values)
def resolve_app_bundle_path():
target_build_dir = os.environ.get('TARGET_BUILD_DIR')
wrapper_name = os.environ.get('WRAPPER_NAME')
if target_build_dir and wrapper_name:
app_bundle_path = os.path.join(target_build_dir, wrapper_name)
if os.path.isdir(app_bundle_path):
return app_bundle_path
return None
def main():
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.chdir(root)
build_secret = load_existing_secret()
if build_secret is None:
build_secret = secrets.token_bytes(32)
generate_swift_secret(build_secret)
print(f'[whitelist] Generated {SECRET_OUTPUT}')
app_bundle_path = resolve_app_bundle_path()
executable_path = os.environ.get('EXECUTABLE_PATH', 'readoor')
if app_bundle_path:
frameworks = scan_bundled_images(app_bundle_path, executable_path)
print(f'[whitelist] Scanned app bundle images from {app_bundle_path}')
else:
frameworks = scan_source_frameworks(FRAMEWORK_SEARCH_PATHS, 'readoor')
print('[whitelist] App bundle not found, fallback to source framework scan')
digest = compute_digest(frameworks, build_secret)
output = {
'known_images': frameworks,
'digest': digest
}
if app_bundle_path:
destination = os.path.join(app_bundle_path, os.path.basename(PLIST_OUTPUT))
os.makedirs(os.path.dirname(destination), exist_ok=True)
with open(destination, 'wb') as f:
plistlib.dump(output, f)
print(f'[whitelist] Generated app bundle plist {destination} with {len(frameworks)} dynamic entries')
else:
os.makedirs(os.path.dirname(PLIST_OUTPUT), exist_ok=True)
with open(PLIST_OUTPUT, 'wb') as f:
plistlib.dump(output, f)
print(f'[whitelist] Generated {PLIST_OUTPUT} with {len(frameworks)} dynamic entries')
print(f'[whitelist] digest={digest[:16]}...')
return 0
if __name__ == '__main__':
sys.exit(main())