Verify all approved voice PCM directly in actual APK with negative controls

This commit is contained in:
NINI
2026-10-06 23:33:24 +09:00
parent 8b2f2d6489
commit 931b5f42e7
4 changed files with 181 additions and 2 deletions
+95
View File
@@ -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()