203 lines
7.1 KiB
Python
203 lines
7.1 KiB
Python
#!/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())
|