Verify all approved voice PCM directly in actual APK with negative controls
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
"""Read actual APK AudioClip payloads; never modify the APK or project assets."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import wave
|
||||
import uuid
|
||||
import zipfile
|
||||
|
||||
import UnityPy
|
||||
|
||||
|
||||
def sha(data):
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def pcm(wav_bytes):
|
||||
with wave.open(io.BytesIO(wav_bytes)) as wav:
|
||||
return ((wav.getnchannels(), wav.getsampwidth(), wav.getframerate(), wav.getnframes()),
|
||||
wav.readframes(wav.getnframes()))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--apk', required=True)
|
||||
parser.add_argument('--project', required=True)
|
||||
parser.add_argument('--evidence', required=True)
|
||||
args = parser.parse_args()
|
||||
project, apk, evidence = Path(args.project), Path(args.apk), Path(args.evidence)
|
||||
evidence.mkdir(parents=True, exist_ok=True)
|
||||
version = importlib.metadata.version('UnityPy')
|
||||
require(version == '1.25.4', f'Expected pinned QA inspector1.25.4, got {version}')
|
||||
adoption = json.loads((project / 'SourceAssets/Audio/Voice/active-adoption.json').read_text(encoding='utf-8-sig'))
|
||||
require(len(adoption['clips']) == 44 and len({c['id'] for c in adoption['clips']}) == 44
|
||||
and adoption['approvedCount'] == 44 and adoption['draftCount'] == 0, '44 unique approved adoption IDs required')
|
||||
adopted = {c['id']: c for c in adoption['clips']}
|
||||
extracted = evidence / ('apk-serialized-data-' + uuid.uuid4().hex)
|
||||
extracted.mkdir()
|
||||
record = {'apkSha256': sha(apk.read_bytes()), 'tool': 'UnityPy', 'toolVersion': version,
|
||||
'method': 'Join actual APK split assets; deserialize AudioClips; decode PCM16; exact PCM SHA and format comparison to adopted WAV',
|
||||
'status': 'failed', 'checks': [], 'extractedEntries': []}
|
||||
try:
|
||||
with zipfile.ZipFile(apk) as archive:
|
||||
entries = [n for n in archive.namelist() if n.startswith('assets/bin/Data/')
|
||||
and not n.endswith('/') and '/Managed/' not in n]
|
||||
names = sorted({re.sub(r'\.split\d+$', '', n) for n in entries})
|
||||
for name in names:
|
||||
parts = sorted([n for n in entries if re.sub(r'\.split\d+$', '', n) == name],
|
||||
key=lambda n: int(n.rsplit('.split', 1)[1]) if '.split' in n else -1)
|
||||
if '.split' in parts[0]:
|
||||
require([int(n.rsplit('.split', 1)[1]) for n in parts] == list(range(len(parts))), 'Contiguous split asset parts required')
|
||||
data = b''.join(archive.read(n) for n in parts)
|
||||
path = extracted / PurePosixPath(name).name
|
||||
path.write_bytes(data)
|
||||
record['extractedEntries'].append({'parts': parts, 'joinedFile': str(path), 'sha256': sha(data)})
|
||||
clips = [obj.read() for obj in UnityPy.load(str(extracted)).objects if obj.type.name == 'AudioClip']
|
||||
record['actualClipCount'] = len(clips)
|
||||
require(len(clips) == 44 and len({clip.m_Name for clip in clips}) == 44, 'Actual APK must contain44 unique AudioClips')
|
||||
require({clip.m_Name for clip in clips} == set(adopted), 'Actual APK clip IDs must exactly equal adoption IDs')
|
||||
for clip in sorted(clips, key=lambda c: c.m_Name):
|
||||
adoption_clip = adopted[clip.m_Name]
|
||||
runtime = (project / adoption_clip['runtimeFile']).read_bytes()
|
||||
require(sha(runtime) == adoption_clip['runtimeSha256'].lower(), 'Adopted runtime SHA mismatch: ' + clip.m_Name)
|
||||
samples = clip.samples
|
||||
require(len(samples) == 1, 'Exactly one PCM payload required: ' + clip.m_Name)
|
||||
packed_wave = next(iter(samples.values()))
|
||||
packed_format, packed_pcm = pcm(packed_wave)
|
||||
runtime_format, runtime_pcm = pcm(runtime)
|
||||
passed = (packed_format == runtime_format and packed_format[:3] == (1, 2, 44100)
|
||||
and sha(packed_pcm) == sha(runtime_pcm) and clip.m_CompressionFormat == 0)
|
||||
record['checks'].append({'id': clip.m_Name, 'passed': passed,
|
||||
'runtimeSha256': sha(runtime), 'apkPcmSha256': sha(packed_pcm),
|
||||
'runtimePcmSha256': sha(runtime_pcm), 'format': packed_format,
|
||||
'resource': clip.m_Resource.m_Source, 'offset': clip.m_Resource.m_Offset,
|
||||
'resourceSize': clip.m_Resource.m_Size})
|
||||
require(passed, f'Actual packed voice PCM mismatch: {clip.m_Name}')
|
||||
record['status'] = 'passed'
|
||||
except Exception as exc:
|
||||
record['error'] = str(exc)
|
||||
raise
|
||||
finally:
|
||||
(evidence / 'apk-voice-payload.json').write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print(f'Actual APK voice PCM exact matches: {len(record["checks"])}/44')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user