VulpesOS/tundra/tools/backup-beatles-boot.py
2026-10-22 20:42:26 +02:00

69 lines
4 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
"""Read boot/recovery via the KaiOS debugger. Never writes phone partitions."""
import argparse
import base64
from datetime import date
import hashlib
import json
import os
from pathlib import Path
import subprocess
from kaios_debug import KaiOSDebugger
ROOT = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path,
default=ROOT / 'out/beatles/backups' / date.today().strftime('%Y%m%d'))
folder = parser.parse_args().output
folder.mkdir(parents=True, exist_ok=True)
os.chmod(folder, 0o700)
lines = subprocess.check_output(['adb', 'devices', '-l'], text=True).splitlines()
serials = [line.split()[0] for line in lines
if len(line.split()) > 2 and line.split()[1] == 'device'
and 'model:Nokia_2720_Flip' in line.split()]
assert len(serials) == 1
adb = ['adb', '-s', serials[0]]
assert subprocess.check_output(adb + ['shell', 'getprop ro.product.codename'], text=True).strip() == 'beatles'
firmware = subprocess.check_output(adb + ['shell', 'getprop ro.build.version.incremental'], text=True).strip()
if firmware != '40.00.17.01':
raise RuntimeError('Partition map only qualified for firmware 40.00.17.01')
parts = subprocess.check_output(adb + ['shell', 'cat /proc/partitions'], text=True)
sizes = {r.split()[3]: int(r.split()[2]) * 1024 for r in parts.splitlines() if len(r.split()) == 4 and r.split()[0].isdigit()}
selected = {'boot': 'mmcblk0p27', 'recovery': 'mmcblk0p28', 'aboot': 'mmcblk0p9', 'abootbak': 'mmcblk0p10'}
report = {'device': 'beatles', 'firmware': firmware, 'method': 'read-only XPCOM file stream through enabled debugger',
'stockFirmwareClaimed': False, 'restorationTested': False, 'partitions': {}}
with KaiOSDebugger(serials[0]) as client:
console = client.request('root', 'getProcess', id=0)['form']['consoleActor']
for name, block in selected.items():
output = folder / (name + '.img')
if output.exists():
raise RuntimeError('Backup already exists: ' + str(output))
target = '/dev/block/bootdevice/by-name/' + name
resolved = subprocess.check_output(adb + ['shell', 'readlink ' + target], text=True).strip()
assert resolved == '/dev/block/' + block
partial = output.with_suffix('.img.partial')
assert not partial.exists()
digest = hashlib.sha256()
count = 0
setup = '''(function(){var f=Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);f.initWithPath(%s);var s=Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);s.init(f,1,0,0);var b=Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);b.setInputStream(s);window.__vulpesBootBackup=b;return "opened-read-only";})()''' % json.dumps(target)
assert client.evaluate(console, setup) == 'opened-read-only'
try:
with partial.open('xb') as stream:
os.chmod(partial, 0o600)
while count < sizes[block]:
length = min(131072, sizes[block] - count)
encoded = client.evaluate(console, 'btoa(window.__vulpesBootBackup.readBytes(' + str(length) + '))')
data = base64.b64decode(encoded, validate=True)
assert len(data) == length
stream.write(data)
digest.update(data)
count += length
finally:
client.evaluate(console, '(function(){window.__vulpesBootBackup.close();delete window.__vulpesBootBackup;return true;})()')
assert count == sizes[block]
partial.rename(output)
report['partitions'][name] = {'bytes': count, 'sha256': digest.hexdigest(), 'device': target}
(folder / 'backup.json').write_text(json.dumps(report, indent=2) + '\n')
print(name, count, digest.hexdigest(), flush=True)
print('Read-only backups complete. Restoration is not yet qualified.')